diff --git a/src/components/ChatHeader.vue b/src/components/ChatHeader.vue index 15b11aa..71d9b71 100644 --- a/src/components/ChatHeader.vue +++ b/src/components/ChatHeader.vue @@ -28,7 +28,7 @@ -
{{ $tc("room.members", memberCount) }}
+
{{ $t("room.members", memberCount) }}
0) { - return this.$tc('room.invitations', invitationCount); + return this.$t('room.invitations', invitationCount); } const missedMessagesCount = this.$matrix.joinedRooms.reduce((value, r) => ((r.roomId !== this.$matrix.currentRoomId && r.getCanonicalAlias() !== this.$matrix.currentRoomId) ? (value + r.getUnreadNotificationCount("total")) : value), 0); if (missedMessagesCount > 0) { - return this.$tc('room.unseen_messages', missedMessagesCount); + return this.$t('room.unseen_messages', missedMessagesCount); } return ""; }, diff --git a/src/components/RoomExport.vue b/src/components/RoomExport.vue index 9ed3df5..a52a08c 100644 --- a/src/components/RoomExport.vue +++ b/src/components/RoomExport.vue @@ -8,7 +8,7 @@
{{ room.name }}
-
{{ $tc("room.members", room.getJoinedMemberCount()) }}
+
{{ $t("room.members", room.getJoinedMemberCount()) }}
{{ exportDate }} diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js index 533649b..79baa31 100644 --- a/src/components/chatMixin.js +++ b/src/components/chatMixin.js @@ -119,7 +119,7 @@ export default { dayForEvent(event) { let dayDiff = util.dayDiffToday(event.getTs()); if (dayDiff < 7) { - return this.$tc("message.time_ago", dayDiff); + return this.$t("message.time_ago", dayDiff); } else { return util.formatDay(event.getTs()); } diff --git a/src/components/file_mode/FileDropLayout.vue b/src/components/file_mode/FileDropLayout.vue index c3cbe8f..c4e0138 100644 --- a/src/components/file_mode/FileDropLayout.vue +++ b/src/components/file_mode/FileDropLayout.vue @@ -79,7 +79,7 @@
-
{{ $tc((this.messageInput && this.messageInput.length > 0) ? +
{{ $t((this.messageInput && this.messageInput.length > 0) ? "file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}
- {{ $tc("message.seen_by_count", seenBy.length) }} + {{ $t("message.seen_by_count", seenBy.length) }}
- {{ $tc("message.seen_by") }} + {{ $t("message.seen_by") }} @@ -96,7 +96,7 @@ export default { seenByTimeStamp(timestamp) { let dayDiff = utils.dayDiffToday(timestamp); if (dayDiff < 3) { - return this.$tc("message.time_ago", dayDiff) + ' '+utils.formatTime(timestamp); + return this.$t("message.time_ago", dayDiff) + ' '+utils.formatTime(timestamp); } else { return utils.formatTime(timestamp); } diff --git a/src/components/messages/messageMixin.js b/src/components/messages/messageMixin.js index 0f77020..74ae05c 100644 --- a/src/components/messages/messageMixin.js +++ b/src/components/messages/messageMixin.js @@ -306,11 +306,11 @@ export default { if (ti < 60) { s = this.$t("global.time.recently"); } else if (ti < 3600 && Math.round(ti / 60) < 60) { - s = this.$tc("global.time.minutes", Math.round(ti / 60)); + s = this.$t("global.time.minutes", Math.round(ti / 60)); } else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) { - s = this.$tc("global.time.hours", Math.round(ti / 60 / 60)); + s = this.$t("global.time.hours", Math.round(ti / 60 / 60)); } else { - s = this.$tc("global.time.days", Math.round(ti / 60 / 60 / 24)); + s = this.$t("global.time.days", Math.round(ti / 60 / 60 / 24)); } return this.toLocalNumbers(s); }, diff --git a/src/main.js b/src/main.js index a724952..8bb9e8c 100644 --- a/src/main.js +++ b/src/main.js @@ -182,6 +182,8 @@ app.use(i18n); app.use(matrix, { store: store, i18n: i18n }); +app.config.globalProperties.$root = app; + //app.use(matrix); //app.use(config); // app.use(analytics); diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js index a0c9098..613d577 100644 --- a/src/services/matrix.service.js +++ b/src/services/matrix.service.js @@ -1,11 +1,11 @@ -import { reactive, createApp } from "vue"; -// import olm from "@matrix-org/olm/olm_legacy"; -// global.Olm = olm; +import { createApp } from "vue"; import * as sdk from "matrix-js-sdk"; -import { TimelineWindow, EventTimeline } from "matrix-js-sdk"; -import util from "../plugins/utils"; +import { TimelineWindow, EventTimeline, EventStatus } from "matrix-js-sdk"; +import util, { STATE_EVENT_ROOM_DELETED, STATE_EVENT_ROOM_TYPE, ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE, ROOM_TYPE_DEFAULT } from "../plugins/utils"; import User from "../models/user"; -import { LocalStorageCryptoStore } from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store"; +import * as LocalStorageCryptoStoreClass from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store"; + +const LocalStorageCryptoStore = LocalStorageCryptoStoreClass.LocalStorageCryptoStore; export const CHANNEL_POWER_LEVELS = { "m.room.encrypted": 0, // NOTE! Since practically all events in encrypted rooms get sent as "m.room.encrypted" we need to set @@ -64,136 +64,181 @@ export default { return user.user_id; }, - currentUserDisplayName() { + currentUserDisplayName() { if (this.ready) { - const user = this.matrixClient.getUser(this.currentUserId) || {}; - return this.userDisplayName || user.displayName; + const user = this.matrixClient.getUser(this.currentUserId) || {}; + return this.userDisplayName || user.displayName; } return null; - }, - - currentUserHomeServer() { - return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId); - }, - - currentRoomId() { - return this.$store.state.currentRoomId; - }, - - joinedRooms() { - return this.rooms.filter((room) => { - return room.selfMembership === "join"; - }); - }, - - invites() { - return this.rooms.filter((room) => { - return room.selfMembership === "invite"; - }); - }, }, - - watch: { - currentRoomId: { + + currentUserMXDomain() { + return User.domainPart(this.currentUserId) || this.$config.defaultMatrixDomainPart; + }, + + currentRoomId() { + return this.$store.state.currentRoomId; + }, + + joinedRooms() { + return this.rooms.filter((room) => { + return room.selfMembership === "join"; + }); + }, + + invites() { + return this.rooms.filter((room) => { + return room.selfMembership === "invite"; + }); + }, + + joinedAndInvitedRooms() { + return this.rooms.filter((room) => { + return room.selfMembership === "join" || room.selfMembership === "invite"; + }); + }, + }, + + watch: { + currentRoomId: { immediate: true, handler(roomId) { - this.currentRoom = this.getRoom(roomId); + this.currentRoom = this.getRoom(roomId); }, - }, }, - - methods: { - createCryptoStore() { + currentRoom: { + immediate: true, + handler(room) { + if (room) { + this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(room.roomId, this.currentUserId); + this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(room.roomId, this.currentUserId); + } else { + this.userCanSendMessageInCurrentRoom = true; + this.userCanSendReactionAndAnswerPollInCurrentRoom = true; + } + }, + }, + }, + + methods: { + createCryptoStore() { console.log("create crypto store"); return new LocalStorageCryptoStore(this.$store.getters.storage); - }, - login(user) { - const tempMatrixClient = sdk.createClient({ - baseUrl: user.home_server, - }); - var promiseLogin; - - const self = this; - if (user.access_token) { - // Logged in on "real" account - promiseLogin = Promise.resolve(user); - } else if (user.is_guest && !user.user_id) { - // 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 user = util.randomUser(this.$config.userIdPrefix); - const pass = util.randomPass(); - promiseLogin = tempMatrixClient - .register(user, pass, null, { - type: "m.login.dummy", - initial_device_display_name: this.$config.appName, - }) - .then((response) => { - console.log("Response", 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; + }, + login(user, registrationFlowHandler, createUser = false) { + return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => { + const tempMatrixClient = sdk.createClient({ + baseUrl: baseUrl, + idBaseUrl: this.$config.identityServer, }); - } 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); + 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.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; + }); } - u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response. - this.$store.commit("setUser", u); - return u; - }); - } - - return promiseLogin.then((user) => { - return self.getMatrixClient(user); + + return promiseLogin.then((user) => { + return self.getMatrixClient(user); + }); }); - }, - - clearCryptoStore() { + }, + + clearCryptoStore() { // Clear crypto related data // TODO - for some reason "clearStores" called in "logout" only clears the "account" crypto // data item, not all sessions etc. Why? We need to do that manually here! const toRemove = []; const storage = this.$store.getters.storage; for (let i = 0; i < storage.length; ++i) { - const key = storage.key(i); - if (key.startsWith("crypto.")) toRemove.push(key); + const key = storage.key(i); + if (key.startsWith("crypto.")) toRemove.push(key); } for (const key of toRemove) { - storage.removeItem(key); + storage.removeItem(key); } - }, - - logout() { + }, + + logout() { if (this.matrixClient) { - this.removeMatrixClientListeners(this.matrixClient); - this.matrixClient.stopClient(); - this.matrixClient.clearStores().then(() => { - this.clearCryptoStore(); - }); - this.matrixClient = null; - this.matrixClientReady = false; + this.removeMatrixClientListeners(this.matrixClient); + this.matrixClient.stopClient(); + this.matrixClient.clearStores().then(() => { + this.clearCryptoStore(); + }); + this.matrixClient = null; + this.matrixClientReady = false; } else { - this.clearCryptoStore(); + this.clearCryptoStore(); } - + this.$store.commit("setUser", null); this.$store.commit("setCurrentRoomId", null); this.rooms = []; @@ -201,189 +246,242 @@ export default { this.userAvatar = null; this.currentRoom = null; this.notificationCount = 0; - }, - - initClient() { + }, + + initClient() { this.reloadRooms(); this.matrixClientReady = true; this.matrixClient.emit("Matrix.initialized", this.matrixClient); this.matrixClient - .getProfileInfo(this.currentUserId) - .then((info) => { - console.log("Got user profile: " + JSON.stringify(info)); - this.userDisplayName = info.displayname; - this.userAvatar = info.avatar_url; - }) - .catch((err) => { - console.log("Failed to get user profile: ", err); - }); - }, - - getMatrixClient(user) { + .getProfileInfo(this.currentUserId) + .then((info) => { + console.log("Got user profile: " + JSON.stringify(info)); + this.userDisplayName = info.displayname; + this.userAvatar = info.avatar_url; + }) + .catch((err) => { + console.log("Failed to get user profile: ", err); + }); + }, + + getMatrixClient(user) { if (user === undefined) { - user = this.$store.state.auth.user; + user = this.$store.state.auth.user; } if (this.matrixClientReady) { - return new Promise((resolve, ignoredreject) => { - resolve(user); - }); - } else if (this.matrixClient) { - return new Promise((resolve, ignoredreject) => { - this.matrixClient.once("Matrix.initialized", (ignoredclient) => { - resolve(user); + return new Promise((resolve, ignoredreject) => { + resolve(user); }); - }); - } - - 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); - // } - console.log("MATRIX CLIENT", this.matrixClient); - - return this.matrixClient - .initCrypto() - .then(() => { - console.log("Crypto initialized"); - - 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"); - } + } else if (this.matrixClient) { + return new Promise((resolve, ignoredreject) => { + this.matrixClient.once("Matrix.initialized", (ignoredclient) => { + resolve(user); }); - }); - } - }) - .then(() => { - // Ready to use! Start by loading rooms. - this.initClient(); - return user; - }); - }, - - /** - * Returns a promise that will log us into the Matrix. - * - * Will use a real account, if we have one, otherwise will create - * a random account. - */ - getLoginPromise() { + }); + } + + const matrixStore = new sdk.MemoryStore(this.$store.getters.storage); + + 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, + cryptoStore: this.createCryptoStore() + //useAuthorizationHeader: true + }; + this.matrixClient = sdk.createClient(opts); + // if (user.is_guest) { + // this.matrixClient.setGuest(true); + // } + console.error("Created client", this.matrixClient); + return this.matrixClient + .initRustCrypto() + .then(() => { + console.log("Crypto initialized"); + + 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"); + } + }); + }); + } + }) + .then(() => { + return this.matrixClient.isVersionSupported("v1.11"); + }) + .then((authedMediaSupported) => { + this.useAuthedMedia = authedMediaSupported; + + // Ready to use! Start by loading rooms. + this.initClient(); + return user; + }); + }); + }, + + /** + * Returns a promise that will log us into the Matrix. + * + * Will use a real account, if we have one, otherwise will create + * a random account. + */ + getLoginPromise(registrationFlowHandler) { if (this.ready) { - return Promise.resolve(this.currentUser); + return Promise.resolve(this.currentUser); } - return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true)); - }, - - addMatrixClientListeners(client) { + return this.$store.dispatch("login", { + user: this.currentUser || new User("", "", true), + registrationFlowHandler, + }); + }, + + addMatrixClientListeners(client) { if (client) { - client.setMaxListeners(100); // Increate max number of listeners. - client.on("event", this.onEvent); - client.on("Room", this.onRoom); - client.on("Session.logged_out", this.onSessionLoggedOut); - client.on("Room.myMembership", this.onRoomMyMembership); + client.setMaxListeners(100); // Increate max number of listeners. + client.on("event", this.onEvent); + client.on("Room", this.onRoom); + client.on("Session.logged_out", this.onSessionLoggedOut); + client.on("Room.myMembership", this.onRoomMyMembership); } - }, - - removeMatrixClientListeners(client) { + }, + + removeMatrixClientListeners(client) { if (client) { - client.off("event", this.onEvent); - client.off("Room", this.onRoom); - client.off("Session.logged_out", this.onSessionLoggedOut); - client.off("Room.myMembership", this.onRoomMyMembership); + client.off("event", this.onEvent); + client.off("Room", this.onRoom); + client.off("Session.logged_out", this.onSessionLoggedOut); + client.off("Room.myMembership", this.onRoomMyMembership); } - }, - - onEvent(event) { + }, + + onEvent(event) { switch (event.getType()) { - case "m.room.topic": - { - const room = this.matrixClient.getRoom(event.getRoomId()); - if (room) { - room.topic = event.getContent().topic; - } - } - break; - - case "m.room.avatar": - { - const room = this.matrixClient.getRoom(event.getRoomId()); - if (room) { - room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true); - } - } - break; - - case "m.room.member": - { - if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { - // Don't use this.currentRoomId, may be an alias. We need the real id! - if ( - event.getContent().membership == "leave" && - (event.getPrevContent() || {}).membership == "join" && - event.getStateKey() == this.currentUserId && - event.getSender() != this.currentUserId - ) { - // We were kicked - const wasPurged = event.getContent().reason == "Room Deleted"; - this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1); + case "m.room.topic": + { + const room = this.matrixClient.getRoom(event.getRoomId()); + if (room) { + room["topic"] = event.getContent().topic; + } } - } - } - break; + break; + + case "m.room.avatar": + { + const room = this.matrixClient.getRoom(event.getRoomId()); + if (room) { + Vue.set( + room, + "avatar", + room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia) + ); + } + } + break; + + case "m.room.power_levels": + { + if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { + this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(event.getRoomId(), this.currentUserId); + this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(event.getRoomId(), this.currentUserId); + } + } + break; + + case "m.room.join_rules": + { + const room = this.matrixClient.getRoom(event.getRoomId()); + if (room && room.getJoinRule() == "private" && room.selfMembership == "invite") { + // We have an invite to a room that's now "private"? This is most probably a deleted DM room. + // Reject the invite, i.e. call "leave" on it. + this.matrixClient.leave(room.roomId); + } + } + break; + + case "m.room.canonical_alias": + { + if (this.currentRoomId && this.currentRoomId.startsWith("#") && !this.currentRoom) { + this.currentRoom = this.getRoom(this.currentRoomId); + } + } + break; + + case STATE_EVENT_ROOM_DELETED: + { + const room = this.matrixClient.getRoom(event.getRoomId()); + if (room && room.currentState) { + // Before we do anything, make sure the sender is an admin! + // Also, do not react if WE are the sender, since we are probably + // busy doing the rest of the purging process... + if (room.currentState.maySendStateEvent("m.room.power_levels", event.getSender())) { + if (event.getSender() !== this.currentUserId) { + this.leaveRoomAndNavigate(room.roomId).then(() => { + this.matrixClient.forget(room.roomId, true); + }); + } + } + } + } + break; } this.updateNotificationCount(); - }, - - onRoom(ignoredroom) { - console.log("Got room", ignoredroom); - this.reloadRooms(); - this.updateNotificationCount(); - }, - - onRoomMyMembership(room) { + }, + + onRoom(room) { if (room.selfMembership === "invite") { - // Invitation. Need to call "recalculate" to pick - // up room name, not sure why exactly. - room.recalculate(); + this.matrixClient + .getRoomTags(room.roomId) + .then((reply) => { + if (Object.keys(reply.tags).includes("m.server_notice")) { + room["isServiceNoticeRoom"] = true; + } + }) + .catch((error) => { + console.error(error); + }); } this.reloadRooms(); - }, - - onSessionLoggedOut() { + this.updateNotificationCount(); + }, + + onRoomMyMembership(room) { + if (room.selfMembership === "invite") { + // Invitation. Need to call "recalculate" to pick + // up room name, not sure why exactly. + room.recalculate(); + } + this.reloadRooms(); + }, + + onSessionLoggedOut() { console.log("Logged out!"); if (this.matrixClient) { - this.removeMatrixClientListeners(this.matrixClient); - this.matrixClient.stopClient(); - this.matrixClient = null; - this.matrixClientReady = false; + this.removeMatrixClientListeners(this.matrixClient); + this.matrixClient.stopClient(); + this.matrixClient = null; + this.matrixClientReady = false; } - + // For "real" accounts we totally wipe the user object, but for "guest" // accounts (i.e. created from random data and with password never changed) // we need to hang on to the generated password and use that to login to a new @@ -391,478 +489,822 @@ export default { // Clear the access token var user = this.$store.state.auth.user; if (user.is_guest) { - delete user.access_token; - this.$store.commit("setUser", user); - - // Login again - this.login(user).catch((error) => { - if (error.data.errcode === "M_FORBIDDEN" && this.currentUser.is_guest) { - // Guest account and password don't work. We are in a strange state, probably because - // of server cleanup of accounts or similar. Wipe account and restart... - this.$store.commit("setUser", null); - } + delete user.access_token; + this.$store.commit("setUser", user); + + // Login again + this.login(user).catch((error) => { + if (error.data.errcode === "M_FORBIDDEN" && this.currentUser.is_guest) { + // Guest account and password don't work. We are in a strange state, probably because + // of server cleanup of accounts or similar. Wipe account and restart... + this.$store.commit("setUser", null); + } + this.$store.commit("setCurrentRoomId", null); + this.$navigation.push({ path: "/login" }, -1); + }); + } else { + this.$store.commit("setUser", null); this.$store.commit("setCurrentRoomId", null); this.$navigation.push({ path: "/login" }, -1); - }); - } else { - this.$store.commit("setUser", null); - this.$store.commit("setCurrentRoomId", null); - this.$navigation.push({ path: "/login" }, -1); } - }, - - reloadRooms() { + }, + + reloadRooms() { // TODO - do incremental update instead of replacing the whole array // each time! var updatedRooms = this.matrixClient.getVisibleRooms(); updatedRooms = updatedRooms.filter((room) => { - return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join"); + return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join") && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0; }); updatedRooms.forEach((room) => { - if (!room.avatar) { - room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true); - } + room["avatar"] = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia); }); - console.log("Reload rooms", updatedRooms); - this.rooms = updatedRooms; - const currentRoom = this.getRoom(this.$store.state.currentRoomId); - if (this.currentRoom != currentRoom) { - this.currentRoom = currentRoom; - } - }, - - setCurrentRoomId(roomId) { + this["rooms"] = updatedRooms; + + const resolvedId = + this.currentRoomId && this.currentRoomId.startsWith("#") + ? this.matrixClient.getRoomIdForAlias(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) { this.$store.commit("setCurrentRoomId", roomId); this.currentRoom = this.getRoom(roomId); - }, - - getRoom(roomId) { + }, + + getRoom(roomId) { if (!roomId) { - return null; + return null; } var room = null; if (this.matrixClient) { - const visibleRooms = this.matrixClient.getRooms(); - room = visibleRooms.find((room) => { - if (roomId.startsWith("#")) { - return room.getCanonicalAlias() == roomId; - } - return room.roomId == roomId; - }); + const visibleRooms = this.matrixClient.getRooms(); + room = visibleRooms.find((room) => { + if (roomId.startsWith("#")) { + return room.getCanonicalAlias() == roomId; + } + return room.roomId == roomId; + }); } return room || null; - }, - - /** - * Return all users we are in a "invite" only room with! - */ - getAllFriends() { + }, + + /** + * Return all users we are in a "invite" only room with! + */ + getAllFriends() { var ids = {}; const ret = []; for (const room of this.rooms) { - if (room.selfMembership == "join" && this.getRoomJoinRule(room) == "invite") { - for (const member of room.getJoinedMembers()) { - if (member.userId != this.currentUserId && !ids[member.userId]) { - ids[member.userId] = member; - ret.push(member); - } + if (room.selfMembership == "join" && this.getRoomJoinRule(room) == "invite") { + for (const member of room.getJoinedMembers()) { + if (member.userId != this.currentUserId && !ids[member.userId]) { + ids[member.userId] = member; + ret.push(member); + } + } } - } } ret.sort((a, b) => { - const aName = a.user ? a.user.displayName : a.name; - const bName = b.user ? b.user.displayName : b.name; - return aName.localeCompare(bName); + const aName = a.user ? a.user.displayName : a.name; + const bName = b.user ? b.user.displayName : b.name; + return aName.localeCompare(bName); }); return ret; - }, - - getRoomJoinRule(room) { + }, + + getRoomJoinRule(room) { if (room) { - const joinRules = room.currentState.getStateEvents("m.room.join_rules", ""); - return joinRules && joinRules.getContent().join_rule; + const joinRules = room.currentState.getStateEvents("m.room.join_rules", ""); + return joinRules && joinRules.getContent().join_rule; } return null; - }, - - getRoomHistoryVisibility(room) { + }, + + getRoomHistoryVisibility(room) { if (room) { - const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", ""); - return historyVisibility && historyVisibility.getContent().history_visibility; + const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", ""); + return historyVisibility && historyVisibility.getContent().history_visibility; } return null; - }, - - leaveRoom(roomId) { - return this.matrixClient.leave(roomId, undefined).then(() => { - this.rooms = this.rooms.filter((room) => { - room.roomId != roomId; - }); - this.matrixClient.store.removeRoom(roomId); - //this.matrixClient.forget(roomId, true, undefined); - }); - }, - - /** - * Purge the room with the given id! This means: - * - Make room invite only - * - Disallow guest access - * - Set history visibility to 'joined' - * - Redact all events - * - Kick all members - * @param roomId - */ - purgeRoom(roomId, statusCallback) { - const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices(); - return new Promise((resolve, reject) => { - const room = this.getRoom(roomId); - if (!room) { - reject("Room not found!"); - return; - } - - const timelineWindow = new TimelineWindow(this.matrixClient, room.getUnfilteredTimelineSet(), {}); - const self = this; - - //console.log("Purge: set invite only"); - statusCallback(this.$t("room.purge_set_room_state")); - this.matrixClient - .sendStateEvent(roomId, "m.room.join_rules", { join_rule: "invite" }, "") - .then(() => { - //console.log("Purge: forbid guest access"); - return this.matrixClient.sendStateEvent(roomId, "m.room.guest_access", { guest_access: "forbidden" }, ""); - }) - .then(() => { - //console.log("Purge: set history visibility to 'joined'"); - return this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", { - history_visibility: "joined", - }); - }) - .then(() => { - //console.log("Purge: create timeline"); - return timelineWindow.load(null, 100); - }) - .then(() => { - const getMoreIfAvailable = function _getMoreIfAvailable() { - if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) { - //console.log("Purge: page back"); - return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((ignoredsuccess) => { - return _getMoreIfAvailable.call(self); - }); - } else { - return Promise.resolve("Done"); - } - }.bind(self); - return getMoreIfAvailable(); - }) - .then(() => { - //console.log("Purge: redact events"); - statusCallback(this.$t("room.purge_redacting_events")); - // First ignore unknown device errors - this.matrixClient.setGlobalErrorOnUnknownDevices(false); - var redactionPromises = []; - timelineWindow.getEvents().forEach((event) => { - if (!event.isRedacted() && !event.isRedaction() && !event.isState()) { - // Redact! - redactionPromises.push(this.matrixClient.redactEvent(event.getRoomId(), event.getId())); - } - }); - return Promise.all(redactionPromises); - }) - .then(() => { - //console.log("Purge: kick members"); - statusCallback(this.$t("room.purge_removing_members")); - var joined = room.getMembersWithMembership("join"); - var invited = room.getMembersWithMembership("invite"); - var members = joined.concat(invited); - - var kickPromises = []; - members.forEach((member) => { - if (member.userId != self.currentUserId) { - kickPromises.push(this.matrixClient.kick(roomId, member.userId, "Room Deleted")); - } - }); - return Promise.all(kickPromises); - }) - .then(() => { - statusCallback(null); - this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); - return this.leaveRoom(roomId); - }) - .then(() => { - resolve(true); // Done! - }) - .catch((err) => { - console.error("Error purging room", err); - this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); - reject(err); + }, + + leaveRoom(roomId) { + return this.matrixClient.leave(roomId).then(() => { + this.$store.commit("setCurrentRoomId", null); + this.rooms = this.rooms.filter((room) => { + room.roomId != roomId; }); + //this.matrixClient.store.removeRoom(roomId); + //this.matrixClient.forget(roomId, true, undefined); }); - }, - - /** - * Get a private chat room with the given user. Searches through our rooms to see - * if a suitable room already exists. If not, one is created. - * @param {*} userId The user to chat with. - */ - getOrCreatePrivateChat(userId) { - return new Promise((resolve, reject) => { - for (const room of this.rooms) { - // Is the other member the one we are looking for? - if (this.isDirectRoomWith(room, userId)) { - if (room.getMyMembership() == "invite") { - this.matrixClient.joinRoom(room.roomId); - } else { - var member = room.getMember(userId); - if (member && member.membership != "join") { - // Resend invite - this.matrixClient.invite(room.roomId, userId); - } - } - resolve(room); - return; + }, + + /** + * Leave the room, and if this is the last room we are in, navigate to the "goodbye" page. + * Otherwise, navigate to home. + * @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); } - } - - // No room found, create one - // - const createRoomOptions = { - visibility: "private", // Not listed! - preset: "private_chat", - is_direct: true, - initial_state: [ - { - type: "m.room.encryption", - state_key: "", - content: { - algorithm: "m.megolm.v1.aes-sha2", - }, - }, - { - type: "m.room.guest_access", - state_key: "", - content: { - guest_access: "forbidden", - }, - }, - { - type: "m.room.history_visibility", - state_key: "", - content: { - history_visibility: "joined", - }, - }, - ], - invite: [userId], - }; - return this.matrixClient - .createRoom(createRoomOptions) - .then(({ room_id, room_alias }) => { - resolve(this.getRoom(room_alias || room_id)); - }) - .catch((error) => { - reject(error); - }); }); - }, - - /** - * Return true if this room is a direct room with the given user. - * @param { } room - * @param {*} userId - */ - isDirectRoomWith(room, userId) { - if (room.getJoinRule() == "invite" && room.getMembers().length == 2) { - let other = room.getMember(userId); - if (other) { - if (room.getMyMembership() == "invite" && other.membership == "join") { - return true; - } else if (room.getMyMembership() == "join" && room.canInvite(this.currentUserId)) { - return true; - } else if (room.getMyMembership() == "join" && other.membership == "join") { - return true; + }, + + kickUser(roomId, userId) { + if (this.matrixClient && roomId && userId) { + this.matrixClient.kick(roomId, userId, ""); + } + }, + + banUser(roomId, userId) { + if (this.matrixClient && roomId && userId) { + this.matrixClient.ban(roomId, userId, ""); + } + }, + + makeAdmin(roomId, userId) { + if (this.matrixClient && roomId && userId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (powerLevelEvent) { + this.matrixClient.setPowerLevel(roomId, userId, 100, powerLevelEvent); + } } - } } - return false; - }, - - on(event, handler) { - if (this.matrixClient) { - this.matrixClient.on(event, handler); + }, + + makeModerator(roomId, userId) { + if (this.matrixClient && roomId && userId) { + const room = this.getRoom(roomId); + console.log("Room", room); + if (room && room.currentState) { + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (powerLevelEvent) { + this.matrixClient.setPowerLevel(roomId, userId, 50, powerLevelEvent); + } + } } - }, - - off(event, handler) { - if (this.matrixClient) { - this.matrixClient.off(event, handler); + }, + + revokeModerator(roomId, userId) { + if (this.matrixClient && roomId && userId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (powerLevelEvent) { + this.matrixClient.setPowerLevel(roomId, userId, 0, powerLevelEvent); + } + } } - }, - - setPassword(oldPassword, newPassword) { - if (this.matrixClient && this.currentUser) { - const authDict = { - type: "m.login.password", - identifier: { - type: "m.id.user", - user: this.currentUser.user_id, - }, - // TODO: Remove `user` once servers support proper UIA - // See https://github.com/matrix-org/synapse/issues/5665 - user: this.currentUser.user_id, - password: oldPassword, - }; - const self = this; - return this.matrixClient - .setPassword(authDict, newPassword) - .then(() => { - // Forget password and remove the 'is_guest' flag, we are now a "real" user! - self.currentUser.password = undefined; - self.currentUser.is_guest = false; - self.$store.commit("setUser", self.currentUser); - }) - .then(() => { - return true; - }); + }, + + /** + * Returns true if the current user is joined to the given room. + * @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.getRoomIdForAlias(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); + } } return Promise.resolve(false); - }, - - uploadFile(file, opts) { - return this.matrixClient.uploadContent(file, opts); - }, - - getPublicRoomInfo(roomId) { - if (!roomId) { - return Promise.reject("Invalid parameters"); + }, + + isReadOnlyRoom(roomId) { + if (this.matrixClient && roomId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (powerLevelEvent) { + if (this.roomType(roomId) == ROOM_TYPE_CHANNEL) { + return Object.keys(powerLevelEvent.getContent().events).length == 0; + } + return powerLevelEvent.getContent().events_default > 0; + } + } } - + return false; + }, + + setReadOnlyRoom(roomId, readOnly) { + if (this.matrixClient && roomId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (powerLevelEvent) { + let content = powerLevelEvent.getContent(); + if (this.roomType(roomId) == ROOM_TYPE_CHANNEL) { + content.events = readOnly ? {} : CHANNEL_POWER_LEVELS + } else { + content.events_default = readOnly ? 50 : 0; + } + this.matrixClient.sendStateEvent(room.roomId, "m.room.power_levels", content); + } + } + } + }, + + userCanSendMessageInRoom(roomId, userId) { + if (this.matrixClient && roomId && userId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + let isAdmin = room.currentState.maySendEvent("m.room.power_levels", this.currentUserId); + return isAdmin || (this.roomType(roomId) != ROOM_TYPE_CHANNEL && !this.isReadOnlyRoom(roomId)); + } + } + return true; + }, + + userCanSendReactionAndAnswerPollInRoom(roomId, userId) { + if (this.matrixClient && roomId && userId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + let isAdmin = room.currentState.maySendEvent("m.room.power_levels", this.currentUserId); + return isAdmin || !this.isReadOnlyRoom(roomId); + } + } + return true; + }, + + roomType(roomId) { + if (this.matrixClient && roomId) { + const room = this.getRoom(roomId); + if (room && room.currentState) { + const roomTypeEvent = room.currentState.getStateEvents(STATE_EVENT_ROOM_TYPE, "") || room.currentState.getStateEvents("m.room.create", ""); + if (roomTypeEvent) { + const roomType = roomTypeEvent.getContent().type; + if ([ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE, ROOM_TYPE_CHANNEL].includes(roomType)) { + return roomType; + } + } + } + } + return ROOM_TYPE_DEFAULT; + }, + + /** + * Purge the room with the given id! This means: + * - Make room invite only + * - Disallow guest access + * - Set history visibility to 'joined' + * - Redact all events + * - Kick all members + * @param roomId + */ + purgeRoom(roomId, statusCallback) { + this.currentRoomBeingPurged = true; + + //console.log("Purge room"); + + const sleep = (ms) => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }; + + const withRetry = (codeBlock) => { + return codeBlock().catch((error) => { + if (error && error.errcode == "M_LIMIT_EXCEEDED") { + var retry = 1000; + if (error.data) { + const retryIn = error.data.retry_after_ms; + retry = Math.max(retry, retryIn ? retryIn : 0); + } + console.log("Rate limited, retry in", retry); + return sleep(retry).then(() => withRetry(codeBlock)); + } else { + return Promise.resolve(); + } + }); + }; + + const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices(); + return new Promise((resolve, reject) => { + const room = this.getRoom(roomId); + if (!room) { + reject("Room not found!"); + return; + } + + // Remove any possible pending events + room.getLiveTimeline().getEvents().filter((e) => [EventStatus.ENCRYPTING, sdk.EventStatus.QUEUED].includes(e.status)).forEach((e) => { + //console.log("Cancel pending event!"); + this.matrixClient.cancelPendingEvent(e); + }); + + const timelineWindow = new TimelineWindow(this.matrixClient, room.getUnfilteredTimelineSet(), {}); + const self = this; + + //console.log("Purge: set invite only"); + statusCallback(this.$t("room.purge_set_room_state")); + withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "private" }, "")) + .then(() => { + //console.log("Purge: forbid guest access"); + return withRetry(() => this.matrixClient.sendStateEvent( + roomId, + "m.room.guest_access", + { guest_access: "forbidden" }, + "" + )); + }) + .then(() => { + //console.log("Purge: set history visibility to 'joined'"); + return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "joined", + })); + }) + .then(() => { + return withRetry(() => this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" })); + }) + .then(() => { + // Set message retention 1 minute (may be limited by server) + return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.retention", { max_lifetime: 60000 })); + }) + .then(() => { + //console.log("Purge: create timeline"); + return timelineWindow.load(null, 100); + }) + .then(() => { + const getMoreIfAvailable = function _getMoreIfAvailable() { + if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) { + //console.log("Purge: page back"); + return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((gotmore) => { + if (gotmore) { + return _getMoreIfAvailable.call(self); + } + return Promise.resolve("Done"); + }); + } else { + return Promise.resolve("Done"); + } + }.bind(self); + return getMoreIfAvailable(); + }) + .then(() => { + //console.log("Purge: redact events"); + statusCallback(this.$t("room.purge_redacting_events")); + // First ignore unknown device errors + this.matrixClient.setGlobalErrorOnUnknownDevices(false); + const allEvents = timelineWindow.getEvents().filter((event) => { + return ( + !event.isRedacted() && + !event.isRedaction() && + !event.isState() && + (!room.currentState || room.currentState.maySendRedactionForEvent(event, this.currentUserId)) + ); + }); + + const redactFirstEvent = (events) => { + statusCallback( + this.$t("room.purge_redacting_events", { + count: allEvents.length - events.length + 1, + total: allEvents.length, + }) + ); + if (events.length == 0) { + return Promise.resolve(true); + } + const event = events[0]; + return withRetry(() => this.matrixClient.redactEvent(event.getRoomId(), event.getId())).then(() => + redactFirstEvent(events.slice(1)) + ); + }; + + return redactFirstEvent(allEvents); + }) + .then(() => { + //console.log("Purge: kick members"); + statusCallback(this.$t("room.purge_removing_members")); + var joined = room.getMembersWithMembership("join"); + var invited = room.getMembersWithMembership("invite"); + var allMembers = joined.concat(invited); + + const me = allMembers.find((m) => m.userId == self.currentUserId); + + const kickFirstMember = (members) => { + //console.log(`Kicking ${members.length} members`); + statusCallback( + this.$t("room.purge_removing_members", { + count: allMembers.length - members.length + 1, + total: allMembers.length, + }) + ); + if (members.length == 0) { + return Promise.resolve(true); + } + const member = members[0]; + if (member.userId == self.currentUserId) { + return kickFirstMember(members.slice(1)); + } else { + // Slight pause to avoid rate limiting. + return sleep(0.1) + .then(() => withRetry(() => { + if (member.membership == "invite" && me && me.powerLevel <= member.powerLevel) { + // The user is invited, but we can't kick them because of power levels. + // Send a new invite with reason set to "Room Deleted". + // The client will be sent stripped room state, and can from that see the + // join_rule of "private". It will then "leave", i.e. reject the invite. + return this.matrixClient.invite(roomId, member.userId, "Room Deleted"); + } + return this.matrixClient.kick(roomId, member.userId, "Room Deleted") + })) + .then(() => kickFirstMember(members.slice(1))); + } + }; + + return kickFirstMember(allMembers); + }) + .then(() => { + return withRetry(() => + this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" }) + ); + }) + .then(() => { + statusCallback(null); + this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); + return withRetry(() => this.leaveRoom(roomId)); + }) + .then(() => { + this.currentRoomBeingPurged = false; + resolve(true); // Done! + }) + .catch((err) => { + console.error("Error purging room", err); + this.currentRoomBeingPurged = false; + this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); + reject(err); + }); + }); + }, + + /** + * Get a private chat room with the given user. Searches through our rooms to see + * if a suitable room already exists. If not, one is created. + * @param {*} userId The user to chat with. + */ + getOrCreatePrivateChat(userId) { + return new Promise((resolve, reject) => { + for (const room of this.rooms) { + // Is the other member the one we are looking for? + if (this.isDirectRoomWith(room, userId)) { + if (room.getMyMembership() == "invite") { + this.matrixClient.joinRoom(room.roomId); + } else { + var member = room.getMember(userId); + if (member && member.membership != "join") { + // Resend invite + this.matrixClient.invite(room.roomId, userId); + } + } + resolve(room); + return; + } + } + + // No room found, create one + // + const createRoomOptions = { + visibility: "private", // Not listed! + preset: "private_chat", + is_direct: true, + initial_state: [ + { + type: "m.room.encryption", + state_key: "", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + { + type: "m.room.guest_access", + state_key: "", + content: { + guest_access: "forbidden", + }, + }, + { + type: "m.room.history_visibility", + state_key: "", + content: { + history_visibility: "invited", + }, + }, + { + type: "m.room.power_levels", + state_key: "", + content: { + users: { + [this.currentUserId]: 100, + [userId]: 100, + }, + }, + }, + ], + invite: [userId], + }; + return this.matrixClient + .createRoom(createRoomOptions) + .then(({ room_id, room_alias }) => { + resolve(this.getRoom(room_alias || room_id)); + }) + .catch((error) => { + reject(error); + }); + }); + }, + + /** + * Return true if this room is a direct room with the given user. + * @param { } room + * @param {*} userId + */ + isDirectRoomWith(room, userId) { + if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) { + let other = room.getMember(userId); + if (other) { + if (room.getMyMembership() == "invite" && other.membership == "join") { + return true; + } else if (room.getMyMembership() == "join" && room.canInvite(this.currentUserId)) { + return true; + } else if (room.getMyMembership() == "join" && other.membership == "join") { + return true; + } + } + } + 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 + */ + isDirectRoom(room) { + // 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 false; + }, + + on(event, handler) { + if (this.matrixClient) { + this.matrixClient.on(event, handler); + } + }, + + off(event, handler) { + if (this.matrixClient) { + this.matrixClient.off(event, handler); + } + }, + + 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)); + } else { + return Promise.reject("No matrix client"); + } + }, + + setPassword(oldPassword, newPassword) { + if (this.matrixClient && this.currentUser) { + const authDict = { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: this.currentUser.user_id, + }, + // TODO: Remove `user` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + user: this.currentUser.user_id, + password: oldPassword, + }; + const self = this; + return this.matrixClient + .setPassword(authDict, newPassword) + .then(() => { + // Forget password and remove the 'is_guest' flag, we are now a "real" user! + self.currentUser.password = undefined; + self.currentUser.is_guest = false; + self.$store.commit("setUser", self.currentUser); + }) + .then(() => { + return true; + }); + } + return Promise.resolve(false); + }, + + uploadFile(file, opts) { + return this.matrixClient.uploadContent(file, opts); + }, + + /** + * Get a matrix client that can be used for public queries. If we are logged in, this is the normal + * matrix client. If not, we create a temp one with a temp password. + * @returns A MatrixClient that can be used for public queries + */ + getPublicQueryMatrixClient() { + if (this.matrixClient) { + return this.getMatrixClient().then(() => { + return this.matrixClient; + }); + } else { + 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 }); + + var clientPromise; + + // 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; + }); + } + + // 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; + }); + } + }, + + getPublicRoomInfo(roomId) { + if (!roomId) { + return Promise.reject("Invalid parameters"); + } + const parts = roomId.split(":"); if (parts.length != 2) { - return Promise.reject("Unknown room server"); + return Promise.reject("Unknown room server"); } const server = parts[1]; - - var clientPromise; - if (this.matrixClient) { - clientPromise = 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); - } - - // 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; - }); - } - - // 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; + + const clientPromise = this.getPublicQueryMatrixClient(); + + const findOrGetMore = function _findOrGetMore(client, useAuthedMedia, response) { + for (var room of response.chunk) { + if ( + (roomId.startsWith("#") && room.canonical_alias == roomId) || + (roomId.startsWith("!") && room.room_id == roomId) + ) { + if (room.avatar_url) { + room["avatar"] = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", false, undefined, useAuthedMedia); + } + return Promise.resolve(room); + } } - 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; - }); - } - - const findOrGetMore = function _findOrGetMore(client, response) { - for (var room of response.chunk) { - if ( - (roomId.startsWith("#") && room.canonical_alias == roomId) || - (roomId.startsWith("!") && room.room_id == roomId) - ) { - if (room.avatar_url) { - room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", true); - } - return Promise.resolve(room); + if (response.next_batch) { + return client + .publicRooms({ + server: server, + limit: 1000, + since: response.next_batch, + }) + .then((response) => { + return _findOrGetMore(client, response); + }) + .catch((err) => { + return Promise.reject("Failed to find room: " + err); + }); + } else { + return Promise.reject("No more data"); } - } - if (response.next_batch) { - return client - .publicRooms({ - server: server, - limit: 1000, - since: response.next_batch, - }) - .then((response) => { - return _findOrGetMore(client, response); - }) - .catch((err) => { - return Promise.reject("Failed to find room: " + err); - }); - } else { - return Promise.reject("No more data"); - } }; - + var matrixClient; + let useAuthedMedia = false; return clientPromise - .then((client) => { - matrixClient = client; - return matrixClient.publicRooms({ server: server, limit: 1000 }); - }) - .then((response) => { - return findOrGetMore(matrixClient, response); - }) - .catch((err) => { - return Promise.reject("Failed to find room: " + err); - }); - }, - - updateNotificationCount() { + .then((client) => { + matrixClient = client; + return client.isVersionSupported("v1.11"); + }) + .then((version1_11) => { + useAuthedMedia = version1_11; + return matrixClient.publicRooms({ server: server, limit: 1000 }); + }) + .then((response) => { + return findOrGetMore(matrixClient, useAuthedMedia, response); + }) + .catch((err) => { + return Promise.reject("Failed to find room: " + err); + }); + }, + + updateNotificationCount() { var count = 0; this.rooms.forEach((room) => { - count += room.getUnreadNotificationCount("total") || 0; + count += room.getUnreadNotificationCount("total") || 0; }); this.notificationCount = count; - }, }, - render: () => {} + + setEventPinned(room, event, pinned) { + if (room && room.currentState && event) { + const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); + const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {} + let pinnedEvents = content["pinned"] || []; + if (pinned && !pinnedEvents.includes(event.getId())) { + pinnedEvents.push(event.getId()); + } else if (!pinned && pinnedEvents.includes(event.getId())) { + pinnedEvents = pinnedEvents.filter((e) => e != event.getId()); + } else { + return; // no change + } + content.pinned = pinnedEvents; + this.matrixClient.sendStateEvent(room.roomId, "m.room.pinned_events", content); + } + }, + + getPinnedEvents(room) { + if (room && room.currentState) { + const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); + const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {} + return content["pinned"] || []; + } else { + return []; + } + }, + }, + render: () => {} }); matrixService.config.globalProperties.$store = store; matrixService.config.globalProperties.$config = app.$config;