From dd70e4a576e9ec74c95afb3cdb5f3a1e4f5e7c4d Mon Sep 17 00:00:00 2001 From: N Pex Date: Fri, 1 Dec 2023 08:20:03 +0000 Subject: [PATCH 1/9] Improved BaseURL and DM link handling --- src/assets/config.json | 6 +- src/components/CreateRoom.vue | 2 +- src/components/GetLink.vue | 51 ++-- src/components/Login.vue | 54 ++-- src/models/user.js | 23 +- src/plugins/utils.js | 36 ++- src/router/index.js | 8 +- src/services/config.service.js | 30 +- src/services/matrix.service.js | 485 +++++++++++++++++---------------- 9 files changed, 373 insertions(+), 322 deletions(-) diff --git a/src/assets/config.json b/src/assets/config.json index 74739ee..f5cb5d7 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -7,7 +7,11 @@ }, "languageSupportEmail": "support@guardianproject.info", "productLink": "letsconvene.im", - "defaultServer": "https://neo.keanu.im", + "defaultBaseUrl": "https://neo.keanu.im", + "matrixDomainPartMapping": { + }, + "useFullyQualifiedDMLinks": true, + "defaultMatrixDomainPart": "neo.keanu.im", "identityServer_unset": "", "registrationToken_unset": "", "rtl": false, diff --git a/src/components/CreateRoom.vue b/src/components/CreateRoom.vue index 64e9818..7825678 100644 --- a/src/components/CreateRoom.vue +++ b/src/components/CreateRoom.vue @@ -463,7 +463,7 @@ export default { .getUniqueAliasForRoomName( this.$matrix.matrixClient, this.roomName, - this.$matrix.currentUserHomeServer + this.$matrix.currentUserMXDomain ) .then((alias) => { createRoomOptions.room_alias_name = alias; diff --git a/src/components/GetLink.vue b/src/components/GetLink.vue index 7a2175b..48574c3 100644 --- a/src/components/GetLink.vue +++ b/src/components/GetLink.vue @@ -53,7 +53,7 @@ import * as sdk from "matrix-js-sdk"; import logoMixin from "./logoMixin"; import InteractiveAuth from './InteractiveAuth.vue'; import CopyLink from "./CopyLink.vue" -import utils from "../plugins/utils"; +import util from "../plugins/utils"; export default { name: "GetLink", @@ -91,7 +91,7 @@ export default { methods: { defaultData() { return { - user: new User(this.$config.defaultServer, "", utils.randomPass()), + user: new User("", util.randomPass()), isValid: false, loading: false, message: "", @@ -128,10 +128,10 @@ export default { var user = Object.assign({}, this.user); - let prefix = userDisplayName.toLowerCase().replaceAll(" ", "-").replaceAll(utils.invalidUserIdChars(), ""); + let prefix = userDisplayName.toLowerCase().replaceAll(" ", "-").replaceAll(util.invalidUserIdChars(), ""); if (prefix.length == 0) { prefix = this.$config.userIdPrefix; - user.user_id = utils.randomUser(prefix); + user.user_id = util.randomUser(prefix); } else { // We first try with a username that is just a processed version of the display name. // If it is already taken, try again with random characters appended. @@ -139,8 +139,6 @@ export default { prefix = prefix + "-"; } - user.normalize(); - this.loading = true; this.loadLoginFlows().then(() => { @@ -160,7 +158,7 @@ export default { this.hasError = true; } else if (error.data && error.data.errcode === 'M_USER_IN_USE') { // Try again with (other/new) random chars appended - user.user_id = utils.randomUser(prefix); + user.user_id = util.randomUser(prefix); return this.$store.dispatch("createUser", { user, registrationFlowHandler: this.$refs.interactiveAuth.registrationFlowHandler }).then( (ignoreduser) => { this.$matrix.setUserDisplayName(userDisplayName); @@ -191,28 +189,29 @@ export default { }, loadLoginFlows() { var user = Object.assign({}, this.user); - user.normalize(); - const server = user.home_server || this.$config.defaultServer; - if (server !== this.currentLoginServer) { - this.currentLoginServer = server; - this.loadingLoginFlows = true; + util.getMatrixBaseUrl(user, this.$config) + .then((baseUrl) => { + if (baseUrl !== this.currentLoginServer) { + this.currentLoginServer = baseUrl; + this.loadingLoginFlows = true; - const matrixClient = sdk.createClient({ baseUrl: server }); - return matrixClient.loginFlows().then((response) => { - console.log("FLOWS", response.flows); - this.loginFlows = response.flows.filter(this.supportedLoginFlow); - this.loadingLoginFlows = false; - if (this.loginFlows.length == 0) { - this.message = this.$t('login.no_supported_flow') - this.hasError = true; + const matrixClient = sdk.createClient({ baseUrl: baseUrl }); + return matrixClient.loginFlows().then((response) => { + console.log("FLOWS", response.flows); + this.loginFlows = response.flows.filter(this.supportedLoginFlow); + this.loadingLoginFlows = false; + if (this.loginFlows.length == 0) { + this.message = this.$t('login.no_supported_flow') + this.hasError = true; + } else { + this.message = ""; + this.hasError = false; + } + }); } else { - this.message = ""; - this.hasError = false; + return Promise.resolve(); } - }); - } else { - return Promise.resolve(); - } + }) }, supportedLoginFlow(flow) { return ["m.login.password"].includes(flow.type); diff --git a/src/components/Login.vue b/src/components/Login.vue index b8056c8..94a6742 100644 --- a/src/components/Login.vue +++ b/src/components/Login.vue @@ -128,7 +128,7 @@ export default { }, data() { return { - user: new User(this.$config.defaultServer, "", ""), + user: new User("", ""), isValid: true, loading: false, message: "", @@ -184,7 +184,6 @@ export default { // Is it a full matrix user id? Modify a copy, so that the UI will still show the full ID. var user = Object.assign({}, this.user); - user.normalize(); this.loading = true; this.$store.dispatch("login", { user }).then( @@ -231,35 +230,36 @@ export default { }, onUsernameBlur() { var user = Object.assign({}, this.user); - user.normalize(); - const server = user.home_server || this.$config.defaultServer; - if (server !== this.currentLoginServer) { - - this.showPasswordField = false; + util.getMatrixBaseUrl(user, this.$config) + .then((baseUrl) => { + if (baseUrl !== this.currentLoginServer) { - this.currentLoginServer = server; - this.loadingLoginFlows = true; + this.showPasswordField = false; - const matrixClient = sdk.createClient({baseUrl: server}); - matrixClient.loginFlows().then((response) => { - console.log("FLOWS", response.flows); - this.loginFlows = response.flows.filter(this.supportedLoginFlow); - this.loadingLoginFlows = false; - if (this.loginFlows.length == 0) { - this.message = this.$t('login.no_supported_flow') - this.hasError = true; - } else { - this.message = ""; - this.hasError = false; - this.showPasswordField = this.loginFlows.some(f => f.type == "m.login.password"); - if (this.showPasswordField) { - this.$nextTick(() => { - this.$refs.password.focus(); - }); - } + this.currentLoginServer = baseUrl; + this.loadingLoginFlows = true; + + const matrixClient = sdk.createClient({ baseUrl: baseUrl }); + matrixClient.loginFlows().then((response) => { + console.log("FLOWS", response.flows); + this.loginFlows = response.flows.filter(this.supportedLoginFlow); + this.loadingLoginFlows = false; + if (this.loginFlows.length == 0) { + this.message = this.$t('login.no_supported_flow') + this.hasError = true; + } else { + this.message = ""; + this.hasError = false; + this.showPasswordField = this.loginFlows.some(f => f.type == "m.login.password"); + if (this.showPasswordField) { + this.$nextTick(() => { + this.$refs.password.focus(); + }); + } + } + }); } }); - } }, supportedLoginFlow(flow) { return ["m.login.password"].includes(flow.type); diff --git a/src/models/user.js b/src/models/user.js index 9b46687..a9f1870 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,19 +1,10 @@ export default class User { - constructor(home_server, user_id, password, is_guest) { - this.home_server = home_server; + constructor(user_id, password, is_guest) { this.user_id = user_id; this.password = password; this.is_guest = is_guest || false } - normalize = function () { - if (this.user_id.startsWith('@') && this.user_id.includes(':')) { - const parts = this.user_id.split(":"); - this.user_id = parts[0].substring(1); - this.home_server = "https://" + parts[1]; - } - }; - static localPart(user_id) { if (user_id && user_id.startsWith('@') && user_id.includes(':')) { const parts = user_id.split(":"); @@ -22,19 +13,11 @@ export default class User { return user_id; } - static serverName(user_id) { + static domainPart(user_id) { if (user_id && user_id.startsWith('@') && user_id.includes(':')) { const parts = user_id.split(":"); return parts[1]; } - return user_id; - } - - // Get the domain out of the home_server, so if that one is e.g. - // "https://yourdomain.com:8008" then we return "yourdomain.com" - static serverDomain(home_server) { - const parts = home_server.split("://"); - const serverAndPort = parts[parts.length - 1].split(/:|\//); - return serverAndPort[0]; + return undefined; } } \ No newline at end of file diff --git a/src/plugins/utils.js b/src/plugins/utils.js index 939fd16..f5dcf15 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -2,6 +2,8 @@ import axios from 'axios'; import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers"; import dataUriToBuffer from "data-uri-to-buffer"; import ImageResize from "image-resize"; +import { AutoDiscovery } from 'matrix-js-sdk'; +import User from '../models/user'; export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice"; export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted"; @@ -867,10 +869,10 @@ class Util { return undefined; } - getUniqueAliasForRoomName(matrixClient, roomName, homeServer, iterationCount) { + getUniqueAliasForRoomName(matrixClient, roomName, defaultMatrixDomainPart, iterationCount) { return new Promise((resolve, reject) => { var preferredAlias = roomName.replace(/\s/g, "").toLowerCase(); - var tryAlias = "#" + preferredAlias + ":" + homeServer; + var tryAlias = "#" + preferredAlias + ":" + defaultMatrixDomainPart; matrixClient.getRoomIdForAlias(tryAlias) .then(ignoredid => { // We got a response, this means the tryAlias already exists. @@ -885,7 +887,7 @@ class Util { roomName = roomName.substring(0, roomName.length - 5); } const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789"); - resolve(this.getUniqueAliasForRoomName(matrixClient, roomName + "-" + randomChars, homeServer, (iterationCount || 0) + 1)) + resolve(this.getUniqueAliasForRoomName(matrixClient, roomName + "-" + randomChars, defaultMatrixDomainPart, (iterationCount || 0) + 1)) }) .catch(err => { if (err.errcode == 'M_NOT_FOUND') { @@ -921,6 +923,34 @@ class Util { console.log("Failed to fetch attachment: ", err); }); } + + getMatrixBaseUrl(user, config) { + if (user) { + const domain = User.domainPart(user.user_id); + if (domain) { + const endpoint = config.getMatrixDomainPartMapping(domain); + if (endpoint) { + console.log("Mapped to", endpoint); + return Promise.resolve(endpoint); + } + return AutoDiscovery.findClientConfig(domain) + .then((clientConfig) => { + const hs = clientConfig['m.homeserver']; + if (hs && !hs.error && hs.base_url) { + console.log("Use home server returned from well-known", hs.base_url); + return hs.base_url; + } + console.log("Fallback to default server"); + return config.defaultBaseUrl; + }) + .catch((err) => { + console.error("Failed well-known lookup", err); + return config.defaultBaseUrl; + }); + } + } + return Promise.resolve(config.defaultBaseUrl); + } } export default new Util(); diff --git a/src/router/index.js b/src/router/index.js index b70280b..903e4c2 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -130,9 +130,9 @@ router.beforeEach((to, from, next) => { if (roomId && !roomId.startsWith("@")) { // Not a full username. Assume local name on this server. return router.app.$config.promise.then((config) => { - const user = new User(config.defaultServer, roomId, ""); - user.normalize(); - roomId = "@" + roomId + ":" + User.serverDomain(user.home_server); + const domain = config.homeServer; + if (!domain) throw new Error("No domain part for user invite!"); + roomId = "@" + roomId + ":" + domain; router.app.$matrix.setCurrentRoomId(roomId); }).catch(err => console.error(err)).finally(() => next()); } else { @@ -186,7 +186,7 @@ router.getRoomLink = function (alias, roomId, roomName, mode) { router.getDMLink = function (user, config) { let userId = user.user_id; - if (user.home_server === config.defaultServer) { + if (User.domainPart(userId) === config.homeServer && !config.useFullyQualifiedDMLinks) { // Using default server, don't include it in the link userId = User.localPart(user.user_id); } diff --git a/src/services/config.service.js b/src/services/config.service.js index 08d5ddc..56dd7aa 100644 --- a/src/services/config.service.js +++ b/src/services/config.service.js @@ -15,8 +15,16 @@ export default { Vue.set(config, key, json[key]); } // If default server is not set, default to current server address - if (!json.defaultServer) { - Vue.set(config, "defaultServer", defaultServerFromLocation); + if (!json.defaultBaseUrl) { + if (json.defaultServer) { + // TODO - Only to migrate old values (defaultServer was renamed defaultBaseUrl), can be removed later... + Vue.set(config, "defaultBaseUrl", defaultServerFromLocation); + } else { + Vue.set(config, "defaultBaseUrl", json.defaultServer); + } + } + if (json.useFullyQualifiedDMLinks == undefined) { + Vue.set(config, "useFullyQualifiedDMLinks", true); // Default to true } Vue.set(config, "loaded", true); @@ -26,6 +34,24 @@ export default { } return config; }); + + /** + * If there is an explicit mapping for this MX domain in the config file, return the endpoint URL that it maps to. + * @param {*} domain + * @returns + */ + config.getMatrixDomainPartMapping = (domain) => { + console.log("Get domain endpoint mapping for", domain); + if (config.matrixDomainPartMapping && config.matrixDomainPartMapping[domain]) { + const mapping = config.matrixDomainPartMapping[domain]; + if (Array.isArray(mapping)) { + return mapping[0]; //TODO - Use the first one for now, but maybe rotate somehow? + } + return mapping; + } + return undefined; + } + Vue.prototype.$config = config; } } diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js index f342881..e163a35 100644 --- a/src/services/matrix.service.js +++ b/src/services/matrix.service.js @@ -68,8 +68,8 @@ export default { return null; }, - currentUserHomeServer() { - return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId); + currentUserMXDomain() { + return User.domainPart(this.currentUserId) || this.$config.defaultMatrixDomainPart; }, currentRoomId() { @@ -92,7 +92,7 @@ export default { return this.rooms.filter((room) => { return room.selfMembership === "join" || room.selfMembership === "invite"; }); - } + }, }, watch: { @@ -110,8 +110,8 @@ export default { } else { this.currentRoomIsReadOnlyForUser = false; } - } - } + }, + }, }, methods: { @@ -120,83 +120,88 @@ export default { return new LocalStorageCryptoStore(this.$store.getters.storage); }, login(user, registrationFlowHandler, createUser = false) { - const tempMatrixClient = sdk.createClient({baseUrl: user.home_server, idBaseUrl: this.$config.identityServer}); - var promiseLogin; - - const self = this; - if (user.access_token) { - // Logged in on "real" account - promiseLogin = Promise.resolve(user); - } else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) { - // Generate random username and password. We don't user REAL matrix - // guest accounts because 1. They are not allowed to post media, 2. They - // can not use avatars and 3. They can not seamlessly be upgraded to real accounts. - // - // Instead, we use an ILAG approach, Improved Landing as Guest. - const userId = (createUser || user.registration_session) ? user.user_id : util.randomUser(this.$config.userIdPrefix); - const pass = (createUser || user.registration_session) ? user.password : util.randomPass(); - - const extractAndSaveUser = (response) => { - var u = Object.assign({}, response); - u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. - u.password = pass; - u.is_guest = true; - this.$store.commit("setUser", u); - return u; - }; - - promiseLogin = tempMatrixClient - .register(userId, pass, user.registration_session || null, { - type: "m.login.dummy", - initial_device_display_name: this.$config.appName, - }) - .then((response) => { - return extractAndSaveUser(response); - }) - .catch(error => { - if (registrationFlowHandler && error.httpStatus == 401 && error.data) { - const registrationSession = error.data.session; - - // Store user, pass and session, so we can resume if network failure occurs etc. - // - var u = {}; - u.user_id = userId; - u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. - u.password = pass; - u.is_guest = true; - u.registration_session = registrationSession; - this.$store.commit("setUser", u); - - return registrationFlowHandler(tempMatrixClient, error.data).then((response) => extractAndSaveUser(response)); - } else { - console.error(error); - } - throw error; - }); - } else { - var data = { - user: User.localPart(user.user_id), - password: user.password, - type: "m.login.password", - initial_device_display_name: this.$config.appName, - }; - if (user.device_id) { - data.device_id = user.device_id; - } - promiseLogin = tempMatrixClient.login("m.login.password", data).then((response) => { - var u = Object.assign({}, response); - if (user.is_guest) { - // Copy over needed properties - u = Object.assign(user, response); - } - u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. - this.$store.commit("setUser", u); - return u; + return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => { + const tempMatrixClient = sdk.createClient({ + baseUrl: baseUrl, + idBaseUrl: this.$config.identityServer, }); - } + var promiseLogin; - return promiseLogin.then((user) => { - return self.getMatrixClient(user); + const self = this; + if (user.access_token) { + // Logged in on "real" account + promiseLogin = Promise.resolve(user); + } else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) { + // Generate random username and password. We don't user REAL matrix + // guest accounts because 1. They are not allowed to post media, 2. They + // can not use avatars and 3. They can not seamlessly be upgraded to real accounts. + // + // Instead, we use an ILAG approach, Improved Landing as Guest. + const userId = + createUser || user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix); + const pass = createUser || user.registration_session ? user.password : util.randomPass(); + + const extractAndSaveUser = (response) => { + var u = Object.assign({}, response); + u.password = pass; + u.is_guest = true; + this.$store.commit("setUser", u); + return u; + }; + + promiseLogin = tempMatrixClient + .register(userId, pass, user.registration_session || null, { + type: "m.login.dummy", + initial_device_display_name: this.$config.appName, + }) + .then((response) => { + return extractAndSaveUser(response); + }) + .catch((error) => { + if (registrationFlowHandler && error.httpStatus == 401 && error.data) { + const registrationSession = error.data.session; + + // Store user, pass and session, so we can resume if network failure occurs etc. + // + var u = {}; + u.user_id = userId; + u.password = pass; + u.is_guest = true; + u.registration_session = registrationSession; + this.$store.commit("setUser", u); + + return registrationFlowHandler(tempMatrixClient, error.data).then((response) => + extractAndSaveUser(response) + ); + } else { + console.error(error); + } + throw error; + }); + } else { + var data = { + user: User.localPart(user.user_id), + password: user.password, + type: "m.login.password", + initial_device_display_name: this.$config.appName, + }; + if (user.device_id) { + data.device_id = user.device_id; + } + promiseLogin = tempMatrixClient.login("m.login.password", data).then((response) => { + var u = Object.assign({}, response); + if (user.is_guest) { + // Copy over needed properties + u = Object.assign(user, response); + } + this.$store.commit("setUser", u); + return u; + }); + } + + return promiseLogin.then((user) => { + return self.getMatrixClient(user); + }); }); }, @@ -271,52 +276,54 @@ export default { const matrixStore = new sdk.MemoryStore(this.$store.getters.storage); - var opts = { - baseUrl: user.home_server, - userId: user.user_id, - store: matrixStore, - deviceId: user.device_id, - accessToken: user.access_token, - timelineSupport: true, - unstableClientRelationAggregation: true, - //useAuthorizationHeader: true - }; - this.matrixClient = sdk.createClient(opts); - // if (user.is_guest) { - // this.matrixClient.setGuest(true); - // } - return this.matrixClient - .initCrypto() - .then(() => { - console.log("Crypto initialized"); + return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => { + var opts = { + baseUrl: baseUrl, + userId: user.user_id, + store: matrixStore, + deviceId: user.device_id, + accessToken: user.access_token, + timelineSupport: true, + unstableClientRelationAggregation: true, + //useAuthorizationHeader: true + }; + this.matrixClient = sdk.createClient(opts); + // if (user.is_guest) { + // this.matrixClient.setGuest(true); + // } + return this.matrixClient + .initCrypto() + .then(() => { + console.log("Crypto initialized"); - this.addMatrixClientListeners(this.matrixClient); + this.addMatrixClientListeners(this.matrixClient); - this.matrixClient.startClient(); - return this.matrixClient; - }) - .then((matrixClient) => { - if (matrixClient.isInitialSyncComplete()) { - console.log("Initial sync done already!"); - return matrixClient; - } else { - return new Promise((resolve, reject) => { - matrixClient.once("sync", function (state, ignoredprevState, ignoredres) { - console.log(state); // state will be 'PREPARED' when the client is ready to use - if (state == "PREPARED") { - resolve(matrixClient); - } else if (state == "ERROR") { - reject("Error syncing"); - } + this.matrixClient.startClient(); + return this.matrixClient; + }) + .then((matrixClient) => { + if (matrixClient.isInitialSyncComplete()) { + console.log("Initial sync done already!"); + return matrixClient; + } else { + return new Promise((resolve, reject) => { + matrixClient.once("sync", function (state, ignoredprevState, ignoredres) { + console.log(state); // state will be 'PREPARED' when the client is ready to use + if (state == "PREPARED") { + resolve(matrixClient); + } else if (state == "ERROR") { + reject("Error syncing"); + } + }); }); - }); - } - }) - .then(() => { - // Ready to use! Start by loading rooms. - this.initClient(); - return user; - }); + } + }) + .then(() => { + // Ready to use! Start by loading rooms. + this.initClient(); + return user; + }); + }); }, /** @@ -329,7 +336,10 @@ export default { if (this.ready) { return Promise.resolve(this.currentUser); } - return this.$store.dispatch("login", { user: this.currentUser || new User(this.$config.defaultServer, "", "", true), registrationFlowHandler }); + return this.$store.dispatch("login", { + user: this.currentUser || new User("", "", true), + registrationFlowHandler, + }); }, addMatrixClientListeners(client) { @@ -376,13 +386,13 @@ export default { break; case "m.room.power_levels": - { - if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { - this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(event.getRoomId(), this.currentUserId); - } + { + if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { + this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(event.getRoomId(), this.currentUserId); } - break; - + } + break; + case STATE_EVENT_ROOM_DELETED: { const room = this.matrixClient.getRoom(event.getRoomId()); @@ -394,7 +404,7 @@ export default { if (event.getSender() !== this.currentUserId) { this.leaveRoomAndNavigate(room.roomId).then(() => { this.matrixClient.store.removeRoom(room.roomId); - }) + }); } } } @@ -406,13 +416,16 @@ export default { onRoom(room) { if (room.selfMembership === "invite") { - this.matrixClient.getRoomTags(room.roomId).then(reply => { - if (Object.keys(reply.tags).includes("m.server_notice")) { - Vue.set(room, "isServiceNoticeRoom", true); - } - }).catch((error => { - console.error(error); - })) + this.matrixClient + .getRoomTags(room.roomId) + .then((reply) => { + if (Object.keys(reply.tags).includes("m.server_notice")) { + Vue.set(room, "isServiceNoticeRoom", true); + } + }) + .catch((error) => { + console.error(error); + }); } this.reloadRooms(); this.updateNotificationCount(); @@ -476,14 +489,19 @@ export default { } }); Vue.set(this, "rooms", updatedRooms); - - const resolvedId = (this.currentRoomId && this.currentRoomId.startsWith("#")) ? this.matrixClient.resolveRoomAlias(this.currentRoomId).then(r => r.room_id) : Promise.resolve(this.currentRoomId); - resolvedId.then(roomId => { - const currentRoom = this.getRoom(roomId); - if (this.currentRoom != currentRoom) { - this.currentRoom = currentRoom; - } - }).catch(ignorederror => {}); + + const resolvedId = + this.currentRoomId && this.currentRoomId.startsWith("#") + ? this.matrixClient.resolveRoomAlias(this.currentRoomId).then((r) => r.room_id) + : Promise.resolve(this.currentRoomId); + resolvedId + .then((roomId) => { + const currentRoom = this.getRoom(roomId); + if (this.currentRoom != currentRoom) { + this.currentRoom = currentRoom; + } + }) + .catch((ignorederror) => {}); }, setCurrentRoomId(roomId) { @@ -562,26 +580,18 @@ export default { /** * Leave the room, and if this is the last room we are in, navigate to the "goodbye" page. * Otherwise, navigate to home. - * @param roomId + * @param roomId */ leaveRoomAndNavigate(roomId) { const joinedRooms = this.joinedRooms; - const isLastRoomWeAreJoinedTo = ( - joinedRooms && - joinedRooms.length == 1 && - joinedRooms[0].roomId == roomId - ); - return this.leaveRoom(roomId) - .then(() => { - if (isLastRoomWeAreJoinedTo) { - this.$navigation.push({ name: "Goodbye" }, -1); - } else { - this.$navigation.push( - { name: "Home", params: { roomId: null } }, - -1 - ); - } - }) + const isLastRoomWeAreJoinedTo = joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == roomId; + return this.leaveRoom(roomId).then(() => { + if (isLastRoomWeAreJoinedTo) { + this.$navigation.push({ name: "Goodbye" }, -1); + } else { + this.$navigation.push({ name: "Home", params: { roomId: null } }, -1); + } + }); }, kickUser(roomId, userId) { @@ -635,18 +645,20 @@ export default { /** * Returns true if the current user is joined to the given room. - * @param roomIdOrAlias + * @param roomIdOrAlias * @returns Promise - Whether the user is joined to the room or not */ isJoinedToRoom(roomIdOrAlias) { if (roomIdOrAlias && this.matrixClient) { try { - const resolvedRoomId = roomIdOrAlias.startsWith("#") ? this.matrixClient.resolveRoomAlias(roomIdOrAlias).then(res => res.room_id) : Promise.resolve(roomIdOrAlias); - return resolvedRoomId.then(roomId => { - return this.matrixClient.getJoinedRooms().then(rooms => { + const resolvedRoomId = roomIdOrAlias.startsWith("#") + ? this.matrixClient.resolveRoomAlias(roomIdOrAlias).then((res) => res.room_id) + : Promise.resolve(roomIdOrAlias); + return resolvedRoomId.then((roomId) => { + return this.matrixClient.getJoinedRooms().then((rooms) => { return rooms.joined_rooms.includes(roomId); }); - }); + }); } catch (ignorederror) { console.error(ignorederror); return Promise.resolve(false); @@ -661,7 +673,7 @@ export default { if (room && room.currentState) { const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (powerLevelEvent) { - return powerLevelEvent.getContent().events_default > 0 + return powerLevelEvent.getContent().events_default > 0; } } } @@ -672,7 +684,7 @@ export default { if (this.matrixClient && roomId && userId) { const room = this.getRoom(roomId); if (room && room.currentState) { - return !room.currentState.maySendMessage(userId) + return !room.currentState.maySendMessage(userId); } } return false; @@ -686,11 +698,7 @@ export default { if (powerLevelEvent) { let content = powerLevelEvent.getContent(); content.events_default = readOnly ? 50 : 0; - this.matrixClient.sendStateEvent( - room.roomId, - "m.room.power_levels", - content - ); + this.matrixClient.sendStateEvent(room.roomId, "m.room.power_levels", content); } } } @@ -759,11 +767,7 @@ export default { }); }) .then(() => { - return this.matrixClient.sendStateEvent( - roomId, - STATE_EVENT_ROOM_DELETED, - { status: "deleted" } - ); + return this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" }); }) .then(() => { //console.log("Purge: create timeline"); @@ -849,11 +853,9 @@ export default { return kickFirstMember(allMembers); }) .then(() => { - return withRetry(() => this.matrixClient.sendStateEvent( - roomId, - STATE_EVENT_ROOM_DELETED, - { status: "deleted" } - )); + return withRetry(() => + this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" }) + ); }) .then(() => { statusCallback(null); @@ -970,7 +972,7 @@ export default { return false; }, - /** + /** * Return true if this room is a direct room with one other user. NOTE: this currently * only checks number of members, not any is_direct flag. * @param { } room @@ -979,7 +981,7 @@ export default { // TODO - Use the is_direct accountData flag (m.direct). WE (as the client) // apprently need to set this... if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) { - return true; + return true; } return false; }, @@ -998,7 +1000,10 @@ export default { setUserDisplayName(name) { if (this.matrixClient) { - return this.matrixClient.setDisplayName(name || this.user.userId).then(() => this.userDisplayName = name).catch(err => console.err("Failed to set display name", err)); + return this.matrixClient + .setDisplayName(name || this.user.userId) + .then(() => (this.userDisplayName = name)) + .catch((err) => console.err("Failed to set display name", err)); } else { return Promise.reject("No matrix client"); } @@ -1043,71 +1048,75 @@ export default { * @returns A MatrixClient that can be used for public queries */ getPublicQueryMatrixClient() { - var clientPromise; if (this.matrixClient) { - clientPromise = this.getMatrixClient().then(() => { + return this.getMatrixClient().then(() => { return this.matrixClient; }); } else { - const tempMatrixClient = sdk.createClient({baseUrl: this.$config.defaultServer}); var tempUserString = this.$store.state.tempuser; var tempUser = null; if (tempUserString) { tempUser = JSON.parse(tempUserString); } + return util.getMatrixBaseUrl(tempUser, this.$config).then((baseUrl) => { + const tempMatrixClient = sdk.createClient({ baseUrl: baseUrl }); - // Need to create an account? - // - if (tempUser) { - clientPromise = Promise.resolve(tempUser); - } else { - const user = util.randomUser(this.$config.userIdPrefix); - const pass = util.randomPass(); - clientPromise = tempMatrixClient - .register(user, pass, null, { - type: "m.login.dummy", - initial_device_display_name: this.$config.appName, - }) - .then((response) => { - console.log("Response", response); - response.password = pass; - response.is_guest = true; - this.$store.commit("setTempUser", response); - return response; - }); - } + var clientPromise; - // Get an access token - clientPromise = clientPromise.then((user) => { - var data = { - user: User.localPart(user.user_id), - password: user.password, - type: "m.login.password", - initial_device_display_name: this.$config.appName, - }; - if (user.device_id) { - data.device_id = user.device_id; + // Need to create an account? + // + if (tempUser) { + clientPromise = Promise.resolve(tempUser); + } else { + const user = util.randomUser(this.$config.userIdPrefix); + const pass = util.randomPass(); + clientPromise = tempMatrixClient + .register(user, pass, null, { + type: "m.login.dummy", + initial_device_display_name: this.$config.appName, + }) + .then((response) => { + console.log("Response", response); + response.password = pass; + response.is_guest = true; + this.$store.commit("setTempUser", response); + return response; + }); } - return tempMatrixClient.login("m.login.password", data); - }); - // Then login - // - // Create a slimmed down client, without crypto. This one is - // Only used to get public room info from. - clientPromise = clientPromise.then((user) => { - var opts = { - baseUrl: this.$config.defaultServer, - userId: user.user_id, - accessToken: user.access_token, - timelineSupport: false, - }; - var matrixClient = sdk.createClient(opts); - matrixClient.startClient(); - return matrixClient; + // Get an access token + clientPromise = clientPromise.then((user) => { + var data = { + user: User.localPart(user.user_id), + password: user.password, + type: "m.login.password", + initial_device_display_name: this.$config.appName, + }; + if (user.device_id) { + data.device_id = user.device_id; + } + return tempMatrixClient.login("m.login.password", data); + }); + + // Then login + // + // Create a slimmed down client, without crypto. This one is + // Only used to get public room info from. + clientPromise = clientPromise.then((user) => { + var opts = { + baseUrl: baseUrl, + userId: user.user_id, + accessToken: user.access_token, + timelineSupport: false, + }; + var matrixClient = sdk.createClient(opts); + matrixClient.startClient(); + return matrixClient; + }); + + return clientPromise; }); } - return clientPromise; }, getPublicUserInfo(userId) { From fd2d0901417909ebaa36f8d0558667f9e403b14b Mon Sep 17 00:00:00 2001 From: N-Pex Date: Mon, 4 Dec 2023 11:40:57 +0100 Subject: [PATCH 2/9] Fix last two occurences of "homeServer" in the code. --- src/router/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/router/index.js b/src/router/index.js index 903e4c2..6be80a1 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -130,7 +130,7 @@ router.beforeEach((to, from, next) => { if (roomId && !roomId.startsWith("@")) { // Not a full username. Assume local name on this server. return router.app.$config.promise.then((config) => { - const domain = config.homeServer; + const domain = config.defaultMatrixDomainPart; if (!domain) throw new Error("No domain part for user invite!"); roomId = "@" + roomId + ":" + domain; router.app.$matrix.setCurrentRoomId(roomId); @@ -186,7 +186,7 @@ router.getRoomLink = function (alias, roomId, roomName, mode) { router.getDMLink = function (user, config) { let userId = user.user_id; - if (User.domainPart(userId) === config.homeServer && !config.useFullyQualifiedDMLinks) { + if (User.domainPart(userId) === config.defaultMatrixDomainPart && !config.useFullyQualifiedDMLinks) { // Using default server, don't include it in the link userId = User.localPart(user.user_id); } From b9c123ecbc583738cd60cc724dc72aa8d320c25c Mon Sep 17 00:00:00 2001 From: N-Pex Date: Tue, 5 Dec 2023 17:23:09 +0100 Subject: [PATCH 3/9] Fix missing return in getlink --- src/components/GetLink.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GetLink.vue b/src/components/GetLink.vue index 48574c3..d062129 100644 --- a/src/components/GetLink.vue +++ b/src/components/GetLink.vue @@ -189,7 +189,7 @@ export default { }, loadLoginFlows() { var user = Object.assign({}, this.user); - util.getMatrixBaseUrl(user, this.$config) + return util.getMatrixBaseUrl(user, this.$config) .then((baseUrl) => { if (baseUrl !== this.currentLoginServer) { this.currentLoginServer = baseUrl; From 3ad766fe1226a9be27cec29833400b452ec924e9 Mon Sep 17 00:00:00 2001 From: 10G Meow <10gmeow@gmail.com> Date: Thu, 7 Dec 2023 20:53:24 +0200 Subject: [PATCH 4/9] Profile settings: add global Notification toggle --- src/App.vue | 6 +- src/assets/translations/en.json | 6 +- src/components/ActionRow.vue | 11 +- src/components/Chat.vue | 61 +---------- src/components/ChatHeader.vue | 5 - src/components/Profile.vue | 108 +++++++++++++++++++- src/plugins/notificationAndServiceWorker.js | 6 +- src/store/index.js | 4 + 8 files changed, 128 insertions(+), 79 deletions(-) diff --git a/src/App.vue b/src/App.vue index e174786..fde972f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -33,6 +33,7 @@ import stickers from "./plugins/stickers"; import { registerServiceWorker, notificationCount, windowNotificationPermission } from "./plugins/notificationAndServiceWorker.js" import logoMixin from "./components/logoMixin"; +import { mapState } from 'vuex' export default { name: "App", @@ -175,13 +176,16 @@ export default { } return favicon; }, + ...mapState([ + 'globalNotification' + ]) }, watch: { notificationCount: { handler(nCount) { // windowNotificationPermission // return value: 'granted', 'default', 'denied' - if (nCount > 0 && this.windowNotificationPermission() === "granted") { + if (this.globalNotification && nCount > 0 && this.windowNotificationPermission() === "granted") { this.showNotification() } } diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index c4398ae..84f83f7 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -218,7 +218,8 @@ "password_new": "New password", "password_repeat": "Repeat new password", "display_name": "Display name", - "display_name_required": "Display name is required" + "display_name_required": "Display name is required", + "notification_label": "Notification" }, "profile_info_popup": { "you_are": "You are", @@ -382,7 +383,8 @@ "body": "Never miss a message or important conversation again! Be notified whenever someone sends you a message or replies to your chat.", "enable": "Enable" }, - "blocked_message": "Notifications blocked. Please reset the permissions" + "blocked_message": "Notification is blocked. Go to your device or browser settings to enable Notification", + "not_supported": "Notification is not yet supported in Mobile" }, "emoji": { "search": "Search...", diff --git a/src/components/ActionRow.vue b/src/components/ActionRow.vue index c175c7d..25e64da 100644 --- a/src/components/ActionRow.vue +++ b/src/components/ActionRow.vue @@ -4,18 +4,16 @@ no-gutters align-content="center" v-on="$listeners" - v-show="icon === 'notifications_active' ? this.windowNotificationPermission() !== 'granted' : true" > - + {{ icon }} - {{ text }} + {{ text }} + diff --git a/src/components/Chat.vue b/src/components/Chat.vue index 857ae52..c9a5fb0 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -3,12 +3,10 @@ - - -
- notifications_active -

- {{ $t("notification.dialog.title") }} -

-
{{ $t("notification.dialog.body") }}
- - - - {{ $t("global.close") }} - - - {{ $t("notification.dialog.enable") }} - - - -
-
@@ -388,7 +347,6 @@ import chatMixin from "./chatMixin"; import sendAttachmentsMixin from "./sendAttachmentsMixin"; import AudioLayout from "./AudioLayout.vue"; import FileDropLayout from "./file_mode/FileDropLayout"; -import { requestNotificationPermission, windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js" import roomTypeMixin from "./roomTypeMixin"; import roomMembersMixin from "./roomMembersMixin"; @@ -525,7 +483,6 @@ export default { Places: this.$t("emoji.categories.places") } }, - notificationDialog: false }; }, @@ -914,18 +871,6 @@ export default { this.initialLoadDone = true; console.log("Loading finished!"); }, - windowNotificationPermission, - onNotificationDialog() { - if(this.windowNotificationPermission() === 'denied') { - alert(this.$t("notification.blocked_message")); - } else if(this.windowNotificationPermission() === 'default') { - this.notificationDialog = true; - } - }, - onNotifyRequest() { - requestNotificationPermission() - this.notificationDialog = false; - }, onRoomJoined(initialEventId) { // Listen to events this.$matrix.on("Room.timeline", this.onEvent); @@ -1157,7 +1102,7 @@ export default { this.onLayoutChange(fn, element); } else { fn(); - } + } } else { fn(); } diff --git a/src/components/ChatHeader.vue b/src/components/ChatHeader.vue index f54610f..c9b4f8d 100644 --- a/src/components/ChatHeader.vue +++ b/src/components/ChatHeader.vue @@ -177,11 +177,6 @@ export default { this.$emit("view-room-details", { event: this.event }); } }); - items.push({ - icon: 'notifications_active', text: this.$t('global.notify'), handler: () => { - this.$emit("notify"); - } - }); items.push({ icon: '$vuetify.icons.ic_member-leave', text: this.$t('leave.leave'), handler: () => { this.leaveRoom(); diff --git a/src/components/Profile.vue b/src/components/Profile.vue index 88caea1..e5d8dba 100644 --- a/src/components/Profile.vue +++ b/src/components/Profile.vue @@ -89,6 +89,16 @@ :icon="'$vuetify.icons.globe'" :text="$t('profile.select_language')" /> + + + @@ -197,6 +207,45 @@ v-model="showSelectLanguageDialog" v-on:close="showSelectLanguageDialog = false" /> + + +
+ notifications_active +

+ {{ $t("notification.dialog.title") }} +

+
{{ $t("notification.dialog.body") }}
+ + + + {{ $t("global.close") }} + + + {{ $t("notification.dialog.enable") }} + + + +
+
@@ -207,6 +256,8 @@ import util from "../plugins/utils"; import profileInfoMixin from "./profileInfoMixin"; import LogoutRoomDialog from './LogoutRoomDialog.vue'; import CopyLink from "./CopyLink.vue" +import { requestNotificationPermission, windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js" +import { mapState } from 'vuex' export default { name: "Profile", @@ -234,7 +285,8 @@ export default { passwordErrorMessage: null, isAvatarLoaded: true, loadValue: 0, - newPasswordHasError: false + newPasswordHasError: false, + notificationDialog: false }; }, @@ -252,7 +304,13 @@ export default { this.newPassword2 && this.newPassword1 == this.newPassword2 ); - } + }, + notificationIcon() { + return this.globalNotification ? 'notifications_active' : 'notifications_off'; + }, + ...mapState([ + 'globalNotification' + ]) }, methods: { @@ -306,7 +364,53 @@ export default { console.log("Progress: " + JSON.stringify(progress)); }); }, + updateGlobalNotificationStore(flag) { + this.$store.commit('setGlobalNotification', flag); + }, + windowNotificationPermission, + onUpdateGlobalNotification(showAlertOrDialog = true) { + const permission = this.windowNotificationPermission(); + + switch (permission) { + case 'denied': + this.updateGlobalNotificationStore(false); + if (showAlertOrDialog) { + alert(this.$t("notification.blocked_message")); + } + break; + case 'granted': + this.updateGlobalNotificationStore(!this.globalNotification); + break; + case 'default': + if (showAlertOrDialog) { + this.notificationDialog = true; + } + this.updateGlobalNotificationStore(!this.globalNotification); + break; + default: + alert(this.$t("notification.not_supported")); + } + }, + async onNotifyDialog() { + const permission = await requestNotificationPermission() + if(permission === 'denied') { + this.updateGlobalNotificationStore(false); + alert(this.$t("notification.blocked_message")); + } else { + this.updateGlobalNotificationStore(true); + } + this.notificationDialog = false; + }, + onNotifyDialogClosed() { + this.updateGlobalNotificationStore(false); + this.notificationDialog = false; + } }, + mounted() { + if(this.globalNotification && this.windowNotificationPermission() !== 'granted') { + this.onUpdateGlobalNotification(false); + } + } }; diff --git a/src/plugins/notificationAndServiceWorker.js b/src/plugins/notificationAndServiceWorker.js index 117c97d..b97af51 100644 --- a/src/plugins/notificationAndServiceWorker.js +++ b/src/plugins/notificationAndServiceWorker.js @@ -6,16 +6,16 @@ export function registerServiceWorker() { } } -export function requestNotificationPermission() { +export async function requestNotificationPermission() { if("PushManager" in window) { - window.Notification.requestPermission(); + return Notification?.requestPermission().then((permission) => permission); } else { console.log("No Push API Support!"); } } export function windowNotificationPermission() { - return window.Notification.permission + return window?.Notification?.permission ?? 'Not_supported' } export function notificationCount() { diff --git a/src/store/index.js b/src/store/index.js index 9024acc..4da7ecc 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -41,6 +41,7 @@ const vuexPersistLocalStorage = new VuexPersist({ language: state.language, currentRoomId: state.currentRoomId, hasShownMissedItemsHint: state.hasShownMissedItemsHint, + globalNotification: state.globalNotification, }; } else { return {}; @@ -98,6 +99,9 @@ export default new Vuex.Store({ }, setHasShownMissedItemsHint(state, flag) { state.hasShownMissedItemsHint = flag; + }, + setGlobalNotification(state, flag) { + state.globalNotification = flag; } }, actions: { From db04080463d01323e4eb41bf8689c59a0797b43b Mon Sep 17 00:00:00 2001 From: N-Pex Date: Mon, 27 Nov 2023 16:38:43 +0100 Subject: [PATCH 5/9] Add .apk, .ipa and .zip as allowed file type(s) for media uploads. --- src/components/Chat.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Chat.vue b/src/components/Chat.vue index c9a5fb0..a49466e 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -213,7 +213,7 @@ + accept="image/*, audio/*, video/*, .pdf, .apk, .ipa, .zip" class="d-none" multiple/>
From 324ccd70b3c9efecc844e3f0b9c3a5b9250c312b Mon Sep 17 00:00:00 2001 From: N-Pex Date: Mon, 4 Dec 2023 11:29:23 +0100 Subject: [PATCH 6/9] file types and exports --- package-lock.json | 14 ++-- package.json | 2 +- src/assets/css/chat.scss | 13 +++- src/components/Chat.vue | 3 +- src/components/RoomExport.vue | 38 ++++++++-- src/components/chatMixin.js | 11 +++ src/components/file_mode/ThumbnailView.vue | 31 +++++++- .../messages/MessageIncomingFile.vue | 10 +-- .../messages/MessageIncomingVideo.vue | 9 +++ .../messages/MessageOutgoingFile.vue | 10 +-- .../messages/MessageOutgoingVideo.vue | 14 ++++ src/components/messages/attachmentMixin.js | 18 ++++- .../export/MessageIncomingAudioExport.vue | 6 +- .../export/MessageIncomingFileExport.vue | 51 +++++++++++++ .../export/MessageIncomingVideoExport.vue | 6 +- .../export/MessageOutgoingAudioExport.vue | 6 +- .../export/MessageOutgoingFileExport.vue | 51 +++++++++++++ .../export/MessageOutgoingVideoExport.vue | 6 +- .../export/exportedAttachmentMixin.js | 17 +++++ src/components/sendAttachmentsMixin.js | 2 +- src/plugins/utils.js | 76 +++++++++++++++++-- 21 files changed, 339 insertions(+), 55 deletions(-) create mode 100644 src/components/messages/export/MessageIncomingFileExport.vue create mode 100644 src/components/messages/export/MessageOutgoingFileExport.vue create mode 100644 src/components/messages/export/exportedAttachmentMixin.js diff --git a/package-lock.json b/package-lock.json index a3fe23c..beb2548 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "jszip": "^3.9.1", "linkify-html": "^4.1.0", "linkifyjs": "^4.1.0", - "material-design-icons-iconfont": "^6.1", + "material-design-icons-iconfont": "^6.7.0", "matrix-js-sdk": "^23.4.0", "md-gum-polyfill": "^1.0.0", "mic-recorder-to-mp3": "^2.2.2", @@ -9936,9 +9936,9 @@ } }, "node_modules/material-design-icons-iconfont": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.1.0.tgz", - "integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ==" + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz", + "integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA==" }, "node_modules/matrix-events-sdk": { "version": "0.0.1", @@ -24023,9 +24023,9 @@ } }, "material-design-icons-iconfont": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.1.0.tgz", - "integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ==" + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz", + "integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA==" }, "matrix-events-sdk": { "version": "0.0.1", diff --git a/package.json b/package.json index ee3deaa..86de8c5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "jszip": "^3.9.1", "linkify-html": "^4.1.0", "linkifyjs": "^4.1.0", - "material-design-icons-iconfont": "^6.1", + "material-design-icons-iconfont": "^6.7.0", "matrix-js-sdk": "^23.4.0", "md-gum-polyfill": "^1.0.0", "mic-recorder-to-mp3": "^2.2.2", diff --git a/src/assets/css/chat.scss b/src/assets/css/chat.scss index 984a969..acd71e7 100644 --- a/src/assets/css/chat.scss +++ b/src/assets/css/chat.scss @@ -783,12 +783,23 @@ body { width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); - align-items: center; display: flex; + flex-direction: column; + align-items: center; + justify-content: center; .download-text { width: 100%; color: white; } + .download-size { + font-size: 70%; + color: white; + } + .download-icon { + width: 32px; + height: 32px; + color: white; + } } .room-name, diff --git a/src/components/Chat.vue b/src/components/Chat.vue index a49466e..a34378b 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -213,7 +213,7 @@ + accept="image/*,audio/*,video/*,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/>
@@ -1545,6 +1545,7 @@ export default { }, download(event) { + console.error("DOWNLOAD!!!"); if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) { const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype)); children.forEach(child => util.download(this.$matrix.matrixClient, child)); diff --git a/src/components/RoomExport.vue b/src/components/RoomExport.vue index 769af2a..6f7b872 100644 --- a/src/components/RoomExport.vue +++ b/src/components/RoomExport.vue @@ -287,6 +287,7 @@ export default { var imageFolder = zip.folder("images"); var audioFolder = zip.folder("audio"); var videoFolder = zip.folder("video"); + var filesFolder = zip.folder("files"); var downloadPromises = []; let components = this.$refs.exportedEvent; @@ -321,7 +322,8 @@ export default { for (let imageIndex = 0; imageIndex < images.length; imageIndex++) { const img = images[imageIndex]; img.onerror = undefined; - img.src = './avatars/' + fileName; + img.removeAttribute("src"); + img.setAttribute("data-exported-src", './avatars/' + fileName); } } } @@ -421,13 +423,15 @@ export default { var extension = ".mp3"; let fileName = comp.event.getId() + extension; audioFolder.file(fileName, blob); // TODO calc bytes + //this.$nextTick(() => { let elements = comp.$el.getElementsByTagName("audio"); let element = elements && elements[0]; if (element) { - element.src = "./audio/" + fileName; + element.setAttribute("data-exported-src", "./audio/" + fileName); } this.processedEvents += 1; resolve(true); + //}); }); } }) @@ -449,13 +453,36 @@ export default { var extension = ".mp4"; let fileName = comp.event.getId() + extension; videoFolder.file(fileName, blob); // TODO calc bytes +// comp.src = "./video/" + fileName; let elements = comp.$el.getElementsByTagName("video"); let element = elements && elements[0]; if (element) { - element.src = "./video/" + fileName; + element.setAttribute("data-exported-src", "./video/" + fileName); } this.processedEvents += 1; - + resolve(true); + }); + } + }) + .catch((ignoredErr) => { + this.processedEvents += 1; + }) + ); + break; + case "MessageIncomingFileExport": + case "MessageOutgoingFileExport": + downloadPromises.push( + util + .getAttachment(this.$matrix.matrixClient, comp.event, null, true) + .then((blob) => { + if (currentMediaSize + blob.size <= maxMediaSize) { + currentMediaSize += blob.size; + return new Promise((resolve, ignoredReject) => { + var extension = util.getFileExtension(comp.event); + let fileName = comp.event.getId() + extension; + filesFolder.file(fileName, blob); + comp.href="./files/" + fileName; + this.processedEvents += 1; resolve(true); }); } @@ -504,7 +531,8 @@ export default { getCssRules(root); this.$nextTick(() => { - doc += this.$refs.exportRoot.outerHTML; + const contentHtml = this.$refs.exportRoot.outerHTML; + doc += contentHtml.replaceAll("data-exported-src=", "src="); doc += "
"; zip.file("chat.html", doc); diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js index c5a4c2a..ce4b85b 100644 --- a/src/components/chatMixin.js +++ b/src/components/chatMixin.js @@ -19,10 +19,12 @@ import MessageIncomingImageExport from "./messages/export/MessageIncomingImageEx import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport"; import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport"; import MessageIncomingThreadExport from "./messages/export/MessageIncomingThreadExport"; +import MessageIncomingFileExport from "./messages/export/MessageIncomingFileExport"; import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport"; import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport"; import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport"; import MessageOutgoingThreadExport from "./messages/export/MessageOutgoingThreadExport"; +import MessageOutgoingFileExport from "./messages/export/MessageOutgoingFileExport"; import ContactJoin from "./messages/ContactJoin.vue"; import ContactLeave from "./messages/ContactLeave.vue"; import ContactInvited from "./messages/ContactInvited.vue"; @@ -172,6 +174,9 @@ export default { event.getContent().info.mimetype && event.getContent().info.mimetype.startsWith("image/svg") ) { + if (isForExport) { + return MessageIncomingFileExport; + } return MessageIncomingFile; } if (isForExport) { @@ -189,6 +194,9 @@ export default { } return MessageIncomingVideo; } else if (event.getContent().msgtype == "m.file") { + if (isForExport) { + return MessageIncomingFileExport; + } return MessageIncomingFile; } else if (stickers.isStickerShortcode(event.getContent().body)) { return MessageIncomingSticker; @@ -223,6 +231,9 @@ export default { } return MessageOutgoingVideo; } else if (event.getContent().msgtype == "m.file") { + if (isForExport) { + return MessageOutgoingFileExport; + } return MessageOutgoingFile; } else if (stickers.isStickerShortcode(event.getContent().body)) { return MessageOutgoingSticker; diff --git a/src/components/file_mode/ThumbnailView.vue b/src/components/file_mode/ThumbnailView.vue index 59a5e76..12003b9 100644 --- a/src/components/file_mode/ThumbnailView.vue +++ b/src/components/file_mode/ThumbnailView.vue @@ -1,19 +1,22 @@ diff --git a/src/components/messages/MessageIncomingFile.vue b/src/components/messages/MessageIncomingFile.vue index 81b25c4..ef34564 100644 --- a/src/components/messages/MessageIncomingFile.vue +++ b/src/components/messages/MessageIncomingFile.vue @@ -10,12 +10,7 @@
- {{ $t('message.file_prefix') }} - + {{ $t('message.edited') }} @@ -25,11 +20,12 @@ diff --git a/src/components/messages/MessageIncomingVideo.vue b/src/components/messages/MessageIncomingVideo.vue index 180d53d..c7af6d8 100644 --- a/src/components/messages/MessageIncomingVideo.vue +++ b/src/components/messages/MessageIncomingVideo.vue @@ -10,6 +10,15 @@ {{ $t('message.download_progress',{percentage: downloadProgress}) }}
+
+
+ {{ fileName }} +
+
+ {{ fileSize }} +
+ download +
diff --git a/src/components/messages/MessageOutgoingFile.vue b/src/components/messages/MessageOutgoingFile.vue index cb13085..87e8e6d 100644 --- a/src/components/messages/MessageOutgoingFile.vue +++ b/src/components/messages/MessageOutgoingFile.vue @@ -11,12 +11,7 @@
- {{ $t('message.file_prefix') }} - + {{ $t('message.edited') }} @@ -26,11 +21,12 @@ diff --git a/src/components/messages/export/MessageIncomingVideoExport.vue b/src/components/messages/export/MessageIncomingVideoExport.vue index 3727ae8..1716a36 100644 --- a/src/components/messages/export/MessageIncomingVideoExport.vue +++ b/src/components/messages/export/MessageIncomingVideoExport.vue @@ -2,7 +2,7 @@
- @@ -11,14 +11,14 @@ diff --git a/src/components/messages/export/MessageOutgoingAudioExport.vue b/src/components/messages/export/MessageOutgoingAudioExport.vue index b0f6906..f32c19a 100644 --- a/src/components/messages/export/MessageOutgoingAudioExport.vue +++ b/src/components/messages/export/MessageOutgoingAudioExport.vue @@ -1,18 +1,18 @@ diff --git a/src/components/messages/export/MessageOutgoingFileExport.vue b/src/components/messages/export/MessageOutgoingFileExport.vue new file mode 100644 index 0000000..0bcaa6f --- /dev/null +++ b/src/components/messages/export/MessageOutgoingFileExport.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/components/messages/export/MessageOutgoingVideoExport.vue b/src/components/messages/export/MessageOutgoingVideoExport.vue index be07c5d..8faf38f 100644 --- a/src/components/messages/export/MessageOutgoingVideoExport.vue +++ b/src/components/messages/export/MessageOutgoingVideoExport.vue @@ -2,7 +2,7 @@
- @@ -11,14 +11,14 @@ diff --git a/src/components/messages/export/exportedAttachmentMixin.js b/src/components/messages/export/exportedAttachmentMixin.js new file mode 100644 index 0000000..e3b72b6 --- /dev/null +++ b/src/components/messages/export/exportedAttachmentMixin.js @@ -0,0 +1,17 @@ +import util from "../../../plugins/utils"; + +export default { + data() { + return { + src: null, + } + }, + computed: { + fileName() { + return util.getFileName(this.event); + }, + fileSize() { + return util.getFileSizeFormatted(this.event); + } + }, +} \ No newline at end of file diff --git a/src/components/sendAttachmentsMixin.js b/src/components/sendAttachmentsMixin.js index 3503a11..25b603b 100644 --- a/src/components/sendAttachmentsMixin.js +++ b/src/components/sendAttachmentsMixin.js @@ -74,7 +74,7 @@ export default { if (item.status !== this.sendStatuses.INITIAL) { return getItemPromise(++index); } - const itemPromise = util.sendImage(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => { + const itemPromise = util.sendFile(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => { if (loaded == total) { item.progress = 100; } else if (total > 0) { diff --git a/src/plugins/utils.js b/src/plugins/utils.js index f5dcf15..5e5d3e4 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -4,6 +4,7 @@ import dataUriToBuffer from "data-uri-to-buffer"; import ImageResize from "image-resize"; import { AutoDiscovery } from 'matrix-js-sdk'; import User from '../models/user'; +const prettyBytes = require("pretty-bytes"); export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice"; export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted"; @@ -159,7 +160,7 @@ class Util { // true // ); url = matrixClient.mxcUrlToHttp(file.url); - } else if (content.file && content.file.url) { + } else if (content.file && content.file.url && this.getMimeType(event).startsWith("image/")) { // No thumb, use real url file = content.file; url = matrixClient.mxcUrlToHttp(file.url); @@ -348,7 +349,7 @@ class Util { }); } - sendImage(matrixClient, roomId, file, onUploadProgress, threadRoot) { + sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) { const uploadPromise = new UploadPromise(undefined); uploadPromise.wrappedPromise = new Promise((resolve, reject) => { var reader = new FileReader(); @@ -371,13 +372,13 @@ class Util { } var description = file.name; - var msgtype = 'm.image'; - if (file.type.startsWith("audio/")) { + var msgtype = 'm.file'; + if (file.type.startsWith("image/")) { + msgtype = 'm.image'; + } else if (file.type.startsWith("audio/")) { msgtype = 'm.audio'; } else if (file.type.startsWith("video/")) { msgtype = 'm.video'; - } else if (file.type.startsWith("application/pdf")) { - msgtype = 'm.file'; } const opts = { @@ -913,7 +914,6 @@ class Util { link.download = event.getContent().body || this.$t("fallbacks.download_name"); document.body.appendChild(link); link.click(); - setTimeout(function () { document.body.removeChild(link); URL.revokeObjectURL(url); @@ -951,6 +951,68 @@ class Util { } return Promise.resolve(config.defaultBaseUrl); } + + getMimeType(event) { + const content = event.getContent(); + return (content.info && content.info.mimetype) ? content.info.mimetype : (content.file && content.file.mimetype) ? content.file.mimetype : ""; + } + + getFileName(event) { + const content = event.getContent(); + return (content.body || content.filename || "").toLowerCase(); + } + + getFileExtension(event) { + const fileName = this.getFileName(event); + const parts = fileName.split("."); + if (parts.length > 1) { + return "." + parts[parts.length - 1].toLowerCase(); + } + return ""; + } + + getFileSize(event) { + const content = event.getContent(); + if (content.info) { + return content.info.size; + } + return 0; + } + + getFileSizeFormatted(event) { + return prettyBytes(this.getFileSize(event)); + } + + isFileTypeAPK(event) { + const mime = this.getMimeType(event); + if (mime === "application/vnd.android.package-archive" || this.getFileName(event).endsWith(".apk")) { + return true; + } + return false; + } + + isFileTypeIPA(event) { + if (this.getFileName(event).endsWith(".ipa")) { + return true; + } + return false; + } + + isFileTypePDF(event) { + const mime = this.getMimeType(event); + if (mime === "application/pdf" || this.getFileName(event).endsWith(".pdf")) { + return true; + } + return false; + } + + isFileTypeZip(event) { + const mime = this.getMimeType(event); + if (["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) || this.getFileName(event).endsWith(".zip")) { + return true; + } + return false; + } } export default new Util(); From db517fbd9c2b78c0949a375897fe61cddef2bb74 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Mon, 4 Dec 2023 15:28:03 +0100 Subject: [PATCH 7/9] Add maxSizeAutoDownloads with default set to 10Mb --- README.md | 1 + src/components/Chat.vue | 2 +- src/components/messages/MessageIncomingImage.vue | 2 +- src/components/messages/MessageIncomingThread.vue | 2 +- src/components/messages/MessageOutgoingImage.vue | 2 +- src/components/messages/MessageOutgoingThread.vue | 2 +- src/components/messages/attachmentMixin.js | 2 +- src/plugins/utils.js | 4 ++-- src/services/config.service.js | 3 +++ 9 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 72b3262..2292d13 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ The app loads runtime configutation from the server at "./config.json" and merge * **logo** - An url or base64-encoded image data url that represents the app logotype. * **accentColor** - The accent color of the app UI. Use a HTML-style color value string, like "#ff0080". * **show_status_messages** - Whether to show only user joins/leaves and display name updates, or the full range of room status updates. Possible values are "never" (only the above), "moderators" (moderators will see all status updates) or "always" (everyone will see all status updates). Defaults to "always". +* **maxSizeAutoDownloads** - Attachments smaller than this will be auto downloaded. Default is 10Mb. ### Sticker short codes - To enable sticker short codes, follow these steps: diff --git a/src/components/Chat.vue b/src/components/Chat.vue index a34378b..5aa0489 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -1507,7 +1507,7 @@ export default { setReplyToImage(event) { util - .getThumbnail(this.$matrix.matrixClient, event) + .getThumbnail(this.$matrix.matrixClient, event, this.$config) .then((url) => { this.replyToImg = url; }) diff --git a/src/components/messages/MessageIncomingImage.vue b/src/components/messages/MessageIncomingImage.vue index b73752f..732c584 100644 --- a/src/components/messages/MessageIncomingImage.vue +++ b/src/components/messages/MessageIncomingImage.vue @@ -39,7 +39,7 @@ export default { const width = this.$refs.image.$el.clientWidth; const height = (width * 9) / 16; util - .getThumbnail(this.$matrix.matrixClient, this.event, width, height) + .getThumbnail(this.$matrix.matrixClient, this.event, this.$config, width, height) .then((url) => { const info = this.event.getContent().info; // JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to diff --git a/src/components/messages/MessageIncomingThread.vue b/src/components/messages/MessageIncomingThread.vue index 85ffc0e..2bab13b 100644 --- a/src/components/messages/MessageIncomingThread.vue +++ b/src/components/messages/MessageIncomingThread.vue @@ -80,7 +80,7 @@ export default { }; ret.promise = util - .getThumbnail(this.$matrix.matrixClient, e, 100, 100) + .getThumbnail(this.$matrix.matrixClient, e, this.$config, 100, 100) .then((url) => { ret.src = url; }) diff --git a/src/components/messages/MessageOutgoingImage.vue b/src/components/messages/MessageOutgoingImage.vue index e9e5c29..3035c71 100644 --- a/src/components/messages/MessageOutgoingImage.vue +++ b/src/components/messages/MessageOutgoingImage.vue @@ -38,7 +38,7 @@ export default { const width = this.$refs.image.$el.clientWidth; const height = (width * 9) / 16; util - .getThumbnail(this.$matrix.matrixClient, this.event, width, height) + .getThumbnail(this.$matrix.matrixClient, this.event, this.$config, width, height) .then((url) => { const info = this.event.getContent().info; // JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to diff --git a/src/components/messages/MessageOutgoingThread.vue b/src/components/messages/MessageOutgoingThread.vue index ca1cda5..ebcf953 100644 --- a/src/components/messages/MessageOutgoingThread.vue +++ b/src/components/messages/MessageOutgoingThread.vue @@ -81,7 +81,7 @@ export default { }; ret.promise = util - .getThumbnail(this.$matrix.matrixClient, e, 100, 100) + .getThumbnail(this.$matrix.matrixClient, e, this.$config, 100, 100) .then((url) => { ret.src = url; }) diff --git a/src/components/messages/attachmentMixin.js b/src/components/messages/attachmentMixin.js index df6f4af..0fed50b 100644 --- a/src/components/messages/attachmentMixin.js +++ b/src/components/messages/attachmentMixin.js @@ -39,7 +39,7 @@ export default { } if (event) { const fileSize = util.getFileSize(event); - if (!userInitiated && (fileSize == 0 || fileSize > 1000000)) { + if (!userInitiated && (fileSize == 0 || fileSize > this.$config.maxSizeAutoDownloads)) { this.userInitiatedDownloadsOnly = true; return; } diff --git a/src/plugins/utils.js b/src/plugins/utils.js index 5e5d3e4..c1b2057 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -130,7 +130,7 @@ class Util { }); } - getThumbnail(matrixClient, event, ignoredw, ignoredh) { + getThumbnail(matrixClient, event, config, ignoredw, ignoredh) { return new Promise((resolve, reject) => { const content = event.getContent(); if (content.url != null) { @@ -160,7 +160,7 @@ class Util { // true // ); url = matrixClient.mxcUrlToHttp(file.url); - } else if (content.file && content.file.url && this.getMimeType(event).startsWith("image/")) { + } else if (content.file && content.file.url && this.getFileSize(event) > 0 && this.getFileSize(event) < config.maxSizeAutoDownloads) { // No thumb, use real url file = content.file; url = matrixClient.mxcUrlToHttp(file.url); diff --git a/src/services/config.service.js b/src/services/config.service.js index 56dd7aa..1885a0d 100644 --- a/src/services/config.service.js +++ b/src/services/config.service.js @@ -26,6 +26,9 @@ export default { if (json.useFullyQualifiedDMLinks == undefined) { Vue.set(config, "useFullyQualifiedDMLinks", true); // Default to true } + if (!json.maxSizeAutoDownloads) { + Vue.set(config, "maxSizeAutoDownloads", 10 * 1024 * 1024); + } Vue.set(config, "loaded", true); // Tell callback we are done loading runtime config From 55e94cee3a354ea916de2b1b935f074ee3b621ad Mon Sep 17 00:00:00 2001 From: N-Pex Date: Tue, 9 Jan 2024 10:38:14 +0100 Subject: [PATCH 8/9] New file type icons --- src/assets/icons/ic_apk.vue | 8 ++++++++ src/assets/icons/ic_ipa.vue | 8 ++++++++ src/assets/icons/ic_pdf.vue | 8 ++++++++ src/assets/icons/ic_zip.vue | 8 ++++++++ src/components/file_mode/ThumbnailView.vue | 8 ++++---- 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/assets/icons/ic_apk.vue create mode 100644 src/assets/icons/ic_ipa.vue create mode 100644 src/assets/icons/ic_pdf.vue create mode 100644 src/assets/icons/ic_zip.vue diff --git a/src/assets/icons/ic_apk.vue b/src/assets/icons/ic_apk.vue new file mode 100644 index 0000000..71d7fa2 --- /dev/null +++ b/src/assets/icons/ic_apk.vue @@ -0,0 +1,8 @@ + diff --git a/src/assets/icons/ic_ipa.vue b/src/assets/icons/ic_ipa.vue new file mode 100644 index 0000000..07419d1 --- /dev/null +++ b/src/assets/icons/ic_ipa.vue @@ -0,0 +1,8 @@ + diff --git a/src/assets/icons/ic_pdf.vue b/src/assets/icons/ic_pdf.vue new file mode 100644 index 0000000..9e2fcb2 --- /dev/null +++ b/src/assets/icons/ic_pdf.vue @@ -0,0 +1,8 @@ + diff --git a/src/assets/icons/ic_zip.vue b/src/assets/icons/ic_zip.vue new file mode 100644 index 0000000..a58cdeb --- /dev/null +++ b/src/assets/icons/ic_zip.vue @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/components/file_mode/ThumbnailView.vue b/src/components/file_mode/ThumbnailView.vue index 12003b9..1756151 100644 --- a/src/components/file_mode/ThumbnailView.vue +++ b/src/components/file_mode/ThumbnailView.vue @@ -38,13 +38,13 @@ export default { computed: { fileTypeIcon() { if (util.isFileTypeAPK(this.item.event)) { - return "phone_android"; + return "$vuetify.icons.ic_apk"; } else if (util.isFileTypeIPA(this.item.event)) { - return "phone_iphone"; + return "$vuetify.icons.ic_ipa"; } else if (util.isFileTypePDF(this.item.event)) { - return "picture_as_pdf"; + return "$vuetify.icons.ic_pdf"; } else if (util.isFileTypeZip(this.item.event)) { - return "folder_zip"; + return "$vuetify.icons.ic_zip"; } return "description" }, From 6454931848ed8f496d683a4746e24a4de16da4dc Mon Sep 17 00:00:00 2001 From: N-Pex Date: Tue, 9 Jan 2024 10:52:16 +0100 Subject: [PATCH 9/9] Remove debug message --- src/components/Chat.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Chat.vue b/src/components/Chat.vue index 5aa0489..e384cdc 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -1545,7 +1545,6 @@ export default { }, download(event) { - console.error("DOWNLOAD!!!"); if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) { const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype)); children.forEach(child => util.download(this.$matrix.matrixClient, child));