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/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/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/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/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/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/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..e384cdc 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -3,12 +3,10 @@ + 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/>
@@ -324,45 +322,6 @@ - - -
- 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(); } @@ -1562,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/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/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..d062129 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; + return 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/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/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..1756151 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/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/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/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/notificationAndServiceWorker.js b/src/plugins/notificationAndServiceWorker.js index 52c3e65..33a20e2 100644 --- a/src/plugins/notificationAndServiceWorker.js +++ b/src/plugins/notificationAndServiceWorker.js @@ -53,16 +53,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/plugins/utils.js b/src/plugins/utils.js index 4575e87..5e9a283 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -2,6 +2,9 @@ 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'; +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"; @@ -127,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) { @@ -157,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.getFileSize(event) > 0 && this.getFileSize(event) < config.maxSizeAutoDownloads) { // No thumb, use real url file = content.file; url = matrixClient.mxcUrlToHttp(file.url); @@ -346,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(); @@ -369,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 = { @@ -867,10 +870,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 +888,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') { @@ -911,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); @@ -932,6 +934,96 @@ class Util { // Check if the user agent matches the pattern for mobile or tablet browsers return mobileTabletPattern.test(userAgent); } + + 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); + } + + 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(); diff --git a/src/router/index.js b/src/router/index.js index b70280b..6be80a1 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.defaultMatrixDomainPart; + 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.defaultMatrixDomainPart && !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..1885a0d 100644 --- a/src/services/config.service.js +++ b/src/services/config.service.js @@ -15,8 +15,19 @@ 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 + } + if (!json.maxSizeAutoDownloads) { + Vue.set(config, "maxSizeAutoDownloads", 10 * 1024 * 1024); } Vue.set(config, "loaded", true); @@ -26,6 +37,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) { 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: {