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 += "