Use vite as bundler

This commit is contained in:
N-Pex 2025-03-31 16:33:54 +02:00
parent 16dc5df9e5
commit b6f7f75fdd
44 changed files with 4308 additions and 15961 deletions

View file

@ -4,8 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico" /> <link rel="icon" id="favicon" href="/favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title> <title></title>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="./icons/icon-72x72.png" sizes="72x72" /> <link rel="apple-touch-icon" href="./icons/icon-72x72.png" sizes="72x72" />
<link rel="apple-touch-icon" href="./icons/icon-96x96.png" sizes="96x96" /> <link rel="apple-touch-icon" href="./icons/icon-96x96.png" sizes="96x96" />
@ -31,11 +31,12 @@
justify-content: center; justify-content: center;
} }
</style> </style>
<script type="module" src="/src/main.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong <strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please >We're sorry but the app doesn't work properly without JavaScript enabled. Please
enable it to continue.</strong enable it to continue.</strong
> >
</noscript> </noscript>

17736
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,25 +3,26 @@
"version": "0.1.44", "version": "0.1.44",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "build": "vite build",
"lint": "vue-cli-service lint", "serve": "vite preview",
"create-sticker-config": "node ./create_sticker_config.js $1" "create-sticker-config": "node ./create_sticker_config.js $1"
}, },
"dependencies": { "dependencies": {
"@matrix-org/olm": "^3.2.12", "@matrix-org/olm": "^3.2.12",
"@vitejs/plugin-vue2": "^2.3.3",
"aes-js": "^3.1.2", "aes-js": "^3.1.2",
"axios": "^1.4.0", "axios": "^1.4.0",
"browserify-fs": "^1.0.0", "browserify-fs": "^1.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"clean-insights-sdk": "^2.4", "clean-insights-sdk": "^2.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.1",
"data-uri-to-buffer": "^3.0.1", "data-uri-to-buffer": "^3.0.1",
"dayjs": "^1.10.3", "dayjs": "^1.10.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fix-webm-duration": "^1.0.0", "fix-webm-duration": "^1.0.0",
"image-resize": "^1.1.5", "image-resize": "^1.4.1",
"image-size": "^1.0.0", "image-size": "^1.0.0",
"intersection-observer": "^0.12", "intersection-observer": "^0.12",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
@ -30,7 +31,7 @@
"linkify-html": "^4.1.0", "linkify-html": "^4.1.0",
"linkifyjs": "^4.1.0", "linkifyjs": "^4.1.0",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"matrix-js-sdk": "^23.4.0", "matrix-js-sdk": "^37.2.0",
"md-gum-polyfill": "^1.0.0", "md-gum-polyfill": "^1.0.0",
"mic-recorder-to-mp3": "^2.2.2", "mic-recorder-to-mp3": "^2.2.2",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
@ -54,15 +55,16 @@
"vuex-persist": "^3.1.3" "vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"eslint": "^7.0", "eslint": "^7.0",
"eslint-plugin-vue": "^7.0", "eslint-plugin-vue": "^7.0",
"sass": "^1.19.0", "rollup-plugin-polyfill-node": "^0.13.0",
"sass": "^1.86.0",
"sass-loader": "^10", "sass-loader": "^10",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"vite-plugin-static-copy": "^2.3.0",
"vue-cli-plugin-vuetify": "^2.5.8", "vue-cli-plugin-vuetify": "^2.5.8",
"vue-template-compiler": "^2.7.16", "vue-template-compiler": "^2.7.16",
"vuetify-loader": "^1.3.0" "vuetify-loader": "^1.3.0"

View file

@ -8,7 +8,7 @@
<div class="typing-users"> <div class="typing-users">
<transition-group name="list" tag="div"> <transition-group name="list" tag="div">
<v-avatar v-for="(member) in recordingMembersExceptMe" :key="member.userId" class="typing-user" size="32" color="grey"> <v-avatar v-for="(member) in recordingMembersExceptMe" :key="member.userId" class="typing-user" size="32" color="grey">
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" /> <AuthedImage v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
member.name.substring(0, 1).toUpperCase() member.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -33,7 +33,7 @@
<div class="typing-users"> <div class="typing-users">
<transition-group name="list" tag="div"> <transition-group name="list" tag="div">
<v-avatar v-for="reaction in reactions" :key="reaction.member.userId" class="typing-user" size="32" color="grey"> <v-avatar v-for="reaction in reactions" :key="reaction.member.userId" class="typing-user" size="32" color="grey">
<img v-if="memberAvatar(reaction.member)" :src="memberAvatar(reaction.member)" /> <AuthedImage v-if="memberAvatar(reaction.member)" :src="memberAvatar(reaction.member)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
reaction.member.name.substring(0, 1).toUpperCase() reaction.member.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -85,10 +85,12 @@
<script> <script>
import messageMixin from "./messages/messageMixin"; import messageMixin from "./messages/messageMixin";
import util from "../plugins/utils"; import util from "../plugins/utils";
import AuthedImage from "./AuthedImage.vue";
import clapping from "@/assets/sounds/clapping.mp3";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin],
components: {}, components: { AuthedImage },
props: { props: {
autoplay: { autoplay: {
type: Boolean, type: Boolean,
@ -270,7 +272,7 @@ export default {
}, },
audioPlaybackReaction(reaction) { audioPlaybackReaction(reaction) {
// Play sound! // Play sound!
const audio = new Audio(require("@/assets/sounds/clapping.mp3")); const audio = new Audio(clapping);
audio.volume = 0.6; audio.volume = 0.6;
audio.play(); audio.play();
@ -407,7 +409,9 @@ export default {
40, 40,
40, 40,
"scale", "scale",
true true,
false,
this.$matrix.useAuthedMedia,
); );
} }
return null; return null;

View file

@ -0,0 +1,56 @@
<template>
<img v-if="imageSrc" :src="imageSrc" />
</template>
<script>
import axios from 'axios';
export default {
name: "AuthedImage",
props: {
src: {
type: String,
default: function () {
return null;
},
},
},
mounted() {
if (this.src) {
console.error("GOT URL", this.src);
if (this.$matrix.useAuthedMedia) {
axios
.get(this.src, { responseType: "blob", headers: {
Authorization: `Bearer ${this.$matrix.matrixClient.getAccessToken()}`,
}})
.then((response) => {
this.imageSrc = URL.createObjectURL(response.data);
})
.catch((err) => {
console.log("Download error: ", err);
});
} else {
this.imageSrc = this.src;
}
}
},
destroyed() {
if (this.imageSrc && this.src != this.imageSrc) {
const url = this.imageSrc;
this.imageSrc = null;
URL.revokeObjectURL(url);
}
},
data() {
return {
imageSrc: null
}
},
methods: {
}
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -4,11 +4,13 @@
v-on:header-click="onHeaderClick" v-on:header-click="onHeaderClick"
v-on:view-room-details="viewRoomDetails" v-on:view-room-details="viewRoomDetails"
v-on:purge="showPurgeConfirmation = true" v-on:purge="showPurgeConfirmation = true"
v-on:download="downloadingChat = true"
v-if="!useFileModeNonAdmin && $matrix.isDirectRoom(room)" /> v-if="!useFileModeNonAdmin && $matrix.isDirectRoom(room)" />
<ChatHeader class="chat-header flex-grow-0 flex-shrink-0" <ChatHeader class="chat-header flex-grow-0 flex-shrink-0"
v-on:header-click="onHeaderClick" v-on:header-click="onHeaderClick"
v-on:view-room-details="viewRoomDetails" v-on:view-room-details="viewRoomDetails"
v-on:purge="showPurgeConfirmation = true" v-on:purge="showPurgeConfirmation = true"
v-on:download="downloadingChat = true"
v-else-if="!useFileModeNonAdmin" /> v-else-if="!useFileModeNonAdmin" />
<AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room" <AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
:events="events" :autoplay="!showRecorder" :events="events" :autoplay="!showRecorder"
@ -311,7 +313,7 @@
</div> </div>
<MessageOperationsBottomSheet ref="messageOperationsSheet"> <MessageOperationsBottomSheet ref="messageOperationsSheet">
<VEmojiPicker ref="emojiPicker" @select="emojiSelected" :i18n="i18nEmoji"/> <!-- <VEmojiPicker ref="emojiPicker" @select="emojiSelected" :i18n="i18nEmoji"/> -->
</MessageOperationsBottomSheet> </MessageOperationsBottomSheet>
<StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" /> <StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />
@ -354,6 +356,8 @@
<!-- PURGE ROOM POPUP --> <!-- PURGE ROOM POPUP -->
<PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" /> <PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" />
<RoomExport :room="room" v-if="downloadingChat" v-on:close="downloadingChat = false" />
<!-- Heart animation --> <!-- Heart animation -->
<div :class="['heart-wrapper', { 'is-active': heartAnimation }]" :style="hearAnimationPosition"> <div :class="['heart-wrapper', { 'is-active': heartAnimation }]" :style="hearAnimationPosition">
<div :class="['heart', { 'is-active': heartAnimation }]" /> <div :class="['heart', { 'is-active': heartAnimation }]" />
@ -390,10 +394,13 @@ import roomMembersMixin from "./roomMembersMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog"; import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageErrorHandler from "./MessageErrorHandler"; import MessageErrorHandler from "./MessageErrorHandler";
import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue'; import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue';
import sizeOf from "image-size";
import dataUriToBuffer from "data-uri-to-buffer";
import prettyBytes from "pretty-bytes";
import RoomExport from "./RoomExport.vue";
//import { VEmojiPicker } from 'v-emoji-picker';
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
const prettyBytes = require("pretty-bytes");
const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */ const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
const WINDOW_BUFFER_SIZE = 0.3; /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position! */ const WINDOW_BUFFER_SIZE = 0.3; /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position! */
@ -445,7 +452,9 @@ export default {
PurgeRoomDialog, PurgeRoomDialog,
WelcomeHeaderChannelUser, WelcomeHeaderChannelUser,
MessageErrorHandler, MessageErrorHandler,
MessageOperationsChannel MessageOperationsChannel,
RoomExport,
//VEmojiPicker
}, },
data() { data() {
@ -540,7 +549,8 @@ export default {
top: 0, top: 0,
left: 0 left: 0
}, },
reverseOrder: false reverseOrder: false,
downloadingChat: false
}; };
}, },
@ -1748,7 +1758,7 @@ export default {
setReplyToImage(event) { setReplyToImage(event) {
util util
.getThumbnail(this.$matrix.matrixClient, event, this.$config) .getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, event, this.$config)
.then((url) => { .then((url) => {
this.replyToImg = url; this.replyToImg = url;
}) })
@ -1796,9 +1806,9 @@ export default {
download(event) { download(event) {
if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) { if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype)); const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
children.forEach(child => util.download(this.$matrix.matrixClient, child)); children.forEach(child => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, child));
} else { } else {
util.download(this.$matrix.matrixClient, event); util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, event);
} }
}, },

View file

@ -6,7 +6,7 @@
class="chat-header-members text-start ma-0 pa-0" class="chat-header-members text-start ma-0 pa-0"
> >
<v-avatar size="48" class="clickable me-2 chat-header-avatar" color="grey" @click.stop="onAvatarClicked"> <v-avatar size="48" class="clickable me-2 chat-header-avatar" color="grey" @click.stop="onAvatarClicked">
<v-img v-if="roomAvatar" :src="roomAvatar" /> <AuthedImage v-if="roomAvatar" :src="roomAvatar" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -33,7 +33,7 @@
<v-col cols="auto" class="text-end ma-0 pa-0 ms-1"> <v-col cols="auto" class="text-end ma-0 pa-0 ms-1">
<v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26" <v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26"
color="#e0e0e0" @click.stop="showProfileInfo = true"> color="#e0e0e0" @click.stop="showProfileInfo = true">
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</v-col> </v-col>
@ -65,8 +65,6 @@
<MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" @close="showMoreMenu = false" <MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" @close="showMoreMenu = false"
v-on:leave="showLeaveConfirmation = true" /> v-on:leave="showLeaveConfirmation = true" />
<RoomExport :room="room" v-if="downloadingChat" v-on:close="downloadingChat = false" />
</v-container> </v-container>
</template> </template>
@ -75,8 +73,7 @@ import LeaveRoomDialog from "../components/LeaveRoomDialog";
import ProfileInfoPopup from "../components/ProfileInfoPopup"; import ProfileInfoPopup from "../components/ProfileInfoPopup";
import MoreMenuPopup from "../components/MoreMenuPopup"; import MoreMenuPopup from "../components/MoreMenuPopup";
import profileInfoMixin from "../components/profileInfoMixin"; import profileInfoMixin from "../components/profileInfoMixin";
import RoomExport from "../components/RoomExport"; import AuthedImage from "./AuthedImage.vue";
import roomInfoMixin from "./roomInfoMixin"; import roomInfoMixin from "./roomInfoMixin";
export default { export default {
@ -86,7 +83,7 @@ export default {
LeaveRoomDialog, LeaveRoomDialog,
ProfileInfoPopup, ProfileInfoPopup,
MoreMenuPopup, MoreMenuPopup,
RoomExport AuthedImage
}, },
data() { data() {
return { return {
@ -94,7 +91,6 @@ export default {
showLeaveConfirmation: false, showLeaveConfirmation: false,
showProfileInfo: false, showProfileInfo: false,
showMoreMenu: false, showMoreMenu: false,
downloadingChat: false,
showMissedItemsInfo: false, showMissedItemsInfo: false,
/** Timer for showing the "missed items" hint */ /** Timer for showing the "missed items" hint */
@ -142,7 +138,7 @@ export default {
if (this.userCanExportChat) { if (this.userCanExportChat) {
items.push({ items.push({
icon: '$vuetify.icons.ic_download', text: this.$t('room_info.download_chat'), handler: () => { icon: '$vuetify.icons.ic_download', text: this.$t('room_info.download_chat'), handler: () => {
this.downloadingChat = true; this.$emit("download", { event: this.event });
} }
}); });
} }
@ -171,7 +167,9 @@ export default {
40, 40,
40, 40,
"scale", "scale",
true true,
false,
this.$matrix.useAuthedMedia
); );
} }
} }

View file

@ -37,7 +37,7 @@
<v-col v-if="$matrix.joinedRooms.length > 1" cols="auto" class="text-end ma-0 pa-0 ms-1"> <v-col v-if="$matrix.joinedRooms.length > 1" cols="auto" class="text-end ma-0 pa-0 ms-1">
<v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26" <v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26"
color="#e0e0e0" @click.stop="showProfileInfo = true"> color="#e0e0e0" @click.stop="showProfileInfo = true">
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</v-col> </v-col>
@ -75,8 +75,6 @@
<MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" @close="showMoreMenu = false" <MoreMenuPopup :show="showMoreMenu" :menuItems="moreMenuItems" @close="showMoreMenu = false"
v-on:leave="showLeaveConfirmation = true" /> v-on:leave="showLeaveConfirmation = true" />
<RoomExport :room="room" v-if="downloadingChat" v-on:close="downloadingChat = false" />
</v-container> </v-container>
</template> </template>
@ -84,8 +82,7 @@
import LeaveRoomDialog from "../components/LeaveRoomDialog"; import LeaveRoomDialog from "../components/LeaveRoomDialog";
import ProfileInfoPopup from "../components/ProfileInfoPopup"; import ProfileInfoPopup from "../components/ProfileInfoPopup";
import MoreMenuPopup from "../components/MoreMenuPopup"; import MoreMenuPopup from "../components/MoreMenuPopup";
import RoomExport from "../components/RoomExport"; import AuthedImage from "./AuthedImage.vue";
import ChatHeader from "./ChatHeader.vue"; import ChatHeader from "./ChatHeader.vue";
export default { export default {
@ -95,7 +92,7 @@ export default {
LeaveRoomDialog, LeaveRoomDialog,
ProfileInfoPopup, ProfileInfoPopup,
MoreMenuPopup, MoreMenuPopup,
RoomExport AuthedImage
}, },
}; };
</script> </script>

View file

@ -38,7 +38,7 @@
> >
<template v-slot:default="{ active }"> <template v-slot:default="{ active }">
<v-list-item-avatar color="grey"> <v-list-item-avatar color="grey">
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" /> <AuthedImage v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
member.name.substring(0, 1).toUpperCase() member.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -64,9 +64,11 @@
<script> <script>
import util from "../plugins/utils"; import util from "../plugins/utils";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "Invite", name: "Invite",
comments: { AuthedImage },
data() { data() {
return { return {
status: "", status: "",
@ -155,7 +157,9 @@ export default {
40, 40,
40, 40,
"scale", "scale",
true true,
false,
this.$matrix.useAuthedMedia
); );
} }
return null; return null;

View file

@ -4,7 +4,7 @@
<div> <div>
<div class="text-center"> <div class="text-center">
<v-avatar class="join-avatar"> <v-avatar class="join-avatar">
<v-img v-if="roomAvatar" :src="roomAvatar" /> <AuthedImage v-if="roomAvatar" :src="roomAvatar" />
<span v-else class="white--text headline"> <span v-else class="white--text headline">
{{ roomName.substring(0, 1).toUpperCase() }} {{ roomName.substring(0, 1).toUpperCase() }}
</span> </span>
@ -50,7 +50,7 @@
{{ $t("join.joining_as") }} {{ $t("join.joining_as") }}
<div class="d-inline-block"> <div class="d-inline-block">
<v-avatar color="#e0e0e0"> <v-avatar color="#e0e0e0">
<v-img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text headline">{{ userAvatarLetter }}</span> <span v-else class="white--text headline">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</div> </div>
@ -134,13 +134,15 @@ import LanguageMixin from "./languageMixin";
import rememberMeMixin from "./rememberMeMixin"; import rememberMeMixin from "./rememberMeMixin";
import logoMixin from "./logoMixin"; import logoMixin from "./logoMixin";
import SelectLanguageDialog from "./SelectLanguageDialog.vue"; import SelectLanguageDialog from "./SelectLanguageDialog.vue";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "Join", name: "Join",
mixins: [LanguageMixin, rememberMeMixin, logoMixin], mixins: [LanguageMixin, rememberMeMixin, logoMixin],
components: { components: {
SelectLanguageDialog, SelectLanguageDialog,
InteractiveAuth InteractiveAuth,
AuthedImage
}, },
data() { data() {
return { return {
@ -194,7 +196,7 @@ export default {
if (!this.$matrix.userAvatar) { if (!this.$matrix.userAvatar) {
return null; return null;
} }
return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true); return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true, undefined, this.$matrix.useAuthedMedia);
}, },
userAvatarLetter() { userAvatarLetter() {

View file

@ -20,6 +20,7 @@ export default {
}, },
errorCaptured(err, ignoredvm, ignoredinfo) { errorCaptured(err, ignoredvm, ignoredinfo) {
this.err = err; this.err = err;
console.error("IGNORE", err, ignoredvm, ignoredinfo);
return false; return false;
} }
}; };

View file

@ -10,7 +10,7 @@
<v-row v-if="showProfile" class="profile-row clickable" @click="viewProfile" no-gutters align-content="center"> <v-row v-if="showProfile" class="profile-row clickable" @click="viewProfile" no-gutters align-content="center">
<v-col cols="auto" class="me-2"> <v-col cols="auto" class="me-2">
<v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile"> <v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile">
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</v-col> </v-col>
@ -28,11 +28,12 @@
<script> <script>
import profileInfoMixin from "./profileInfoMixin"; import profileInfoMixin from "./profileInfoMixin";
import ActionRow from "./ActionRow.vue"; import ActionRow from "./ActionRow.vue";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "MoreMenuPopup", name: "MoreMenuPopup",
mixins: [profileInfoMixin], mixins: [profileInfoMixin],
components: { ActionRow }, components: { ActionRow, AuthedImage },
props: { props: {
show: { show: {
type: Boolean, type: Boolean,

View file

@ -25,7 +25,7 @@
@click="showAvatarPicker" @click="showAvatarPicker"
v-if="isAvatarLoaded" v-if="isAvatarLoaded"
> >
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
<input <input
id="avatar-picker" id="avatar-picker"
@ -261,6 +261,7 @@ import ActionRow from "./ActionRow.vue";
import util from "../plugins/utils"; import util from "../plugins/utils";
import profileInfoMixin from "./profileInfoMixin"; import profileInfoMixin from "./profileInfoMixin";
import LogoutRoomDialog from './LogoutRoomDialog.vue'; import LogoutRoomDialog from './LogoutRoomDialog.vue';
import AuthedImage from "./AuthedImage.vue";
import CopyLink from "./CopyLink.vue" import CopyLink from "./CopyLink.vue"
import { requestNotificationPermission, windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js" import { requestNotificationPermission, windowNotificationPermission } from "../plugins/notificationAndServiceWorker.js"
import { mapState } from 'vuex' import { mapState } from 'vuex'
@ -272,7 +273,8 @@ export default {
ActionRow, ActionRow,
SelectLanguageDialog, SelectLanguageDialog,
LogoutRoomDialog, LogoutRoomDialog,
CopyLink CopyLink,
AuthedImage
}, },
data() { data() {
return { return {

View file

@ -33,7 +33,7 @@
</v-col> </v-col>
<v-col cols="auto" class="pa-2"> <v-col cols="auto" class="pa-2">
<v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile"> <v-avatar class="avatar-32" size="32" color="#e0e0e0" @click.stop="viewProfile">
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</v-col> </v-col>
@ -64,10 +64,12 @@
</template> </template>
<script> <script>
import profileInfoMixin from "./profileInfoMixin"; import profileInfoMixin from "./profileInfoMixin";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "ProfileInfoPopup", name: "ProfileInfoPopup",
mixins: [profileInfoMixin], mixins: [profileInfoMixin],
components: { AuthedImage },
props: { props: {
show: { show: {
type: Boolean, type: Boolean,

View file

@ -68,7 +68,7 @@
size="32" size="32"
color="#e0e0e0" color="#e0e0e0"
> >
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</div> </div>
@ -77,10 +77,12 @@
</template> </template>
<script> <script>
import profileInfoMixin from "./profileInfoMixin"; import profileInfoMixin from "./profileInfoMixin";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "QuoteView", name: "QuoteView",
mixins: [profileInfoMixin], mixins: [profileInfoMixin],
components: { AuthedImage },
props: { props: {
roomWasPurged: { roomWasPurged: {
type: Boolean, type: Boolean,

View file

@ -1,6 +1,6 @@
<template> <template>
<v-avatar :class="{'room-avatar':true, 'cursor-pointer':userCanPurgeRoom}" @click="userCanPurgeRoom?showRoomAvatarPicker():null" v-if="isRoomAvatarLoaded"> <v-avatar :class="{'room-avatar':true, 'cursor-pointer':userCanPurgeRoom}" @click="userCanPurgeRoom?showRoomAvatarPicker():null" v-if="isRoomAvatarLoaded">
<v-img v-if="roomAvatar" :src="roomAvatar"/> <AuthedImage v-if="roomAvatar" :src="roomAvatar"/>
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
roomName.substring(0, 1).toUpperCase() roomName.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -28,10 +28,12 @@
<script> <script>
import util from "../plugins/utils"; import util from "../plugins/utils";
import roomInfoMixin from "./roomInfoMixin"; import roomInfoMixin from "./roomInfoMixin";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "RoomAvatarPicker", name: "RoomAvatarPicker",
mixins: [roomInfoMixin], mixins: [roomInfoMixin],
components: { AuthedImage },
data() { data() {
return { return {
isRoomAvatarLoaded: true, isRoomAvatarLoaded: true,
@ -56,7 +58,10 @@ export default {
self.isRoomAvatarLoaded = true; self.isRoomAvatarLoaded = true;
} }
} }
) ).then((url) => {
console.error("UPDATE AVATAR", url);
this.room.avatar = url;
})
}, },
handleRoomPickedAvatar(event) { handleRoomPickedAvatar(event) {
if (event.target.files && event.target.files[0]) { if (event.target.files && event.target.files[0]) {

View file

@ -328,7 +328,7 @@ export default {
} }
if (!avatarFolder.file(fileName)) { if (!avatarFolder.file(fileName)) {
const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true); const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true, false, this.$matrix.useAuthedMedia);
if (url) { if (url) {
avatarFolder.file(fileName, "empty"); avatarFolder.file(fileName, "empty");
downloadPromises.push( downloadPromises.push(
@ -367,7 +367,7 @@ export default {
downloadPromises.push( downloadPromises.push(
util util
.getAttachment(this.$matrix.matrixClient, comp.event, null, true) .getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => { .then((blob) => {
return new Promise((resolve, ignoredReject) => { return new Promise((resolve, ignoredReject) => {
let mime = blob.type; let mime = blob.type;
@ -413,7 +413,7 @@ export default {
case "MessageOutgoingAudioExport": case "MessageOutgoingAudioExport":
downloadPromises.push( downloadPromises.push(
util util
.getAttachment(this.$matrix.matrixClient, comp.event, null, true) .getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => { .then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) { if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size; currentMediaSize += blob.size;
@ -443,7 +443,7 @@ export default {
case "MessageOutgoingVideoExport": case "MessageOutgoingVideoExport":
downloadPromises.push( downloadPromises.push(
util util
.getAttachment(this.$matrix.matrixClient, comp.event, null, true) .getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => { .then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) { if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size; currentMediaSize += blob.size;
@ -472,7 +472,7 @@ export default {
case "MessageOutgoingFileExport": case "MessageOutgoingFileExport":
downloadPromises.push( downloadPromises.push(
util util
.getAttachment(this.$matrix.matrixClient, comp.event, null, true) .getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => { .then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) { if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size; currentMediaSize += blob.size;

View file

@ -212,7 +212,7 @@
<div> <div>
<div class="user-icon-with-badge"> <div class="user-icon-with-badge">
<v-avatar class="avatar" size="32" color="grey"> <v-avatar class="avatar" size="32" color="grey">
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" /> <AuthedImage v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
member.name.substring(0, 1).toUpperCase() member.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -311,10 +311,12 @@ import ReportRoomDialog from "../components/ReportRoomDialog";
import RoomExport from "../components/RoomExport"; import RoomExport from "../components/RoomExport";
import RoomAvatarPicker from "../components/RoomAvatarPicker"; import RoomAvatarPicker from "../components/RoomAvatarPicker";
import CopyLink from "../components/CopyLink.vue" import CopyLink from "../components/CopyLink.vue"
import UserProfileDialog from "./UserProfileDialog.vue" import UserProfileDialog from "./UserProfileDialog.vue";
import AuthedImage from "./AuthedImage.vue";
import roomInfoMixin from "./roomInfoMixin"; import roomInfoMixin from "./roomInfoMixin";
import roomTypeMixin from "./roomTypeMixin"; import roomTypeMixin from "./roomTypeMixin";
import util, { STATE_EVENT_ROOM_TYPE } from "../plugins/utils"; import util, { STATE_EVENT_ROOM_TYPE } from "../plugins/utils";
import buildVersion from "../assets/version.txt?raw";
export default { export default {
name: "RoomInfo", name: "RoomInfo",
@ -327,7 +329,8 @@ export default {
UserProfileDialog, UserProfileDialog,
RoomExport, RoomExport,
RoomAvatarPicker, RoomAvatarPicker,
CopyLink CopyLink,
AuthedImage
}, },
data() { data() {
return { return {
@ -365,9 +368,7 @@ export default {
this.user = this.$matrix.matrixClient.getUser(this.$matrix.currentUserId); this.user = this.$matrix.matrixClient.getUser(this.$matrix.currentUserId);
// Display build version // Display build version
const version = require("!!raw-loader!../assets/version.txt").default; this.buildVersion = buildVersion;
console.log("Version", version);
this.buildVersion = version;
}, },
destroyed() { destroyed() {

View file

@ -17,7 +17,7 @@
<v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in invitedRooms" :key="room.roomId" <v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in invitedRooms" :key="room.roomId"
:value="room" class="room-list-room"> :value="room" class="room-list-room">
<v-list-item-avatar size="42" color="#d9d9d9"> <v-list-item-avatar size="42" color="#d9d9d9">
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" /> <AuthedImage v-if="roomAvatar(room)" :src="roomAvatar(room)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -36,7 +36,7 @@
<v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room" class="room-list-room"> <v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room" class="room-list-room">
<v-list-item-avatar size="42" color="#d9d9d9" :class="[{'rounded-circle': isDirect(room)}]"> <v-list-item-avatar size="42" color="#d9d9d9" :class="[{'rounded-circle': isDirect(room)}]">
<v-img v-if="roomAvatar(room)" :src="roomAvatar(room)" /> <AuthedImage v-if="roomAvatar(room)" :src="roomAvatar(room)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -61,10 +61,11 @@
<script> <script>
import util from "../plugins/utils"; import util from "../plugins/utils";
import Vue from "vue"; import Vue from "vue";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "RoomList", name: "RoomList",
components: { AuthedImage },
props: { props: {
title: { title: {
type: String, type: String,
@ -111,7 +112,9 @@ export default {
42, 42,
42, 42,
"scale", "scale",
true true,
false,
this.$matrix.useAuthedMedia
); );
} }
} else { } else {

View file

@ -9,7 +9,7 @@
<div class="dialog-content text-center member-action-dialog"> <div class="dialog-content text-center member-action-dialog">
<div class="pt-4"> <div class="pt-4">
<v-avatar class="avatar" size="56" color="grey"> <v-avatar class="avatar" size="56" color="grey">
<img v-if="memberAvatarComp" :src="memberAvatarComp" /> <AuthedImage v-if="memberAvatarComp" :src="memberAvatarComp" />
<span v-else class="white--text headline">{{ firstLetterUserName }}</span> <span v-else class="white--text headline">{{ firstLetterUserName }}</span>
</v-avatar> </v-avatar>
<div> <div>
@ -63,13 +63,15 @@
<script> <script>
import roomInfoMixin from "./roomInfoMixin"; import roomInfoMixin from "./roomInfoMixin";
import DeviceList from "../components/DeviceList"; import DeviceList from "../components/DeviceList";
import AuthedImage from "./AuthedImage.vue";
import util from "../plugins/utils"; import util from "../plugins/utils";
export default { export default {
name: "UserProfileDialog", name: "UserProfileDialog",
mixins: [roomInfoMixin], mixins: [roomInfoMixin],
components: { components: {
DeviceList DeviceList,
AuthedImage
}, },
props: { props: {
show: { show: {

View file

@ -167,10 +167,11 @@ const State = {
}; };
import util from "../plugins/utils"; import util from "../plugins/utils";
import VoiceRecorderLock from "./VoiceRecorderLock"; import VoiceRecorderLock from "./VoiceRecorderLock";
require("md-gum-polyfill"); import "md-gum-polyfill";
import MicRecorder from "mic-recorder-to-mp3"; import MicRecorder from "mic-recorder-to-mp3";
import ysFixWebmDuration from "fix-webm-duration"; import ysFixWebmDuration from "fix-webm-duration";
//import { duration } from "dayjs"; //import { duration } from "dayjs";
import recordStop from "@/assets/sounds/record_stop.mp3";
export default { export default {
name: "VoiceRecorder", name: "VoiceRecorder",
@ -400,7 +401,7 @@ export default {
} }
}, },
playRecordedSound() { playRecordedSound() {
const audio = new Audio(require("@/assets/sounds/record_stop.mp3")); const audio = new Audio(recordStop);
audio.play(); audio.play();
}, },
aquireWakeLock() { aquireWakeLock() {

View file

@ -22,7 +22,7 @@
</i18n> </i18n>
</span> </span>
<v-avatar color="#e0e0e0" right @click.stop="viewProfile"> <v-avatar color="#e0e0e0" right @click.stop="viewProfile">
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text">{{ userAvatarLetter }}</span> <span v-else class="white--text">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
</v-chip> </v-chip>
@ -30,10 +30,12 @@
</template> </template>
<script> <script>
import profileInfoMixin from "./profileInfoMixin"; import profileInfoMixin from "./profileInfoMixin";
import AuthedImage from "./AuthedImage.vue";
export default { export default {
name: "YouAre", name: "YouAre",
mixins: [profileInfoMixin], mixins: [profileInfoMixin],
components: { AuthedImage },
props: { props: {
dark: { dark: {
type: Boolean, type: Boolean,

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="create-room-avatar" color="#ededed"> <div class="create-room-avatar" color="#ededed">
<v-img class="create-room-avatar__icon clickable" @click.stop="showRoomAvatarPicker" v-if="modelValue && modelValue.image" :src="modelValue.image" /> <AuthedImage class="create-room-avatar__icon clickable" @click.stop="showRoomAvatarPicker" v-if="modelValue && modelValue.image" :src="modelValue.image" />
<v-icon class="create-room-avatar__icon default" v-else>$vuetify.icons.room_avatar_placeholder</v-icon> <v-icon class="create-room-avatar__icon default" v-else>$vuetify.icons.room_avatar_placeholder</v-icon>
<v-icon class="create-room-avatar__camera clickable" v-if="!modelValue || !modelValue.image" @click.stop="showRoomAvatarPicker">$vuetify.icons.ic_camera</v-icon> <v-icon class="create-room-avatar__camera clickable" v-if="!modelValue || !modelValue.image" @click.stop="showRoomAvatarPicker">$vuetify.icons.ic_camera</v-icon>
<input id="room-avatar-picker" ref="roomAvatar" type="file" name="roomAvatar" <input id="room-avatar-picker" ref="roomAvatar" type="file" name="roomAvatar"
@ -9,8 +9,11 @@
</template> </template>
<script> <script>
import AuthedImage from '../AuthedImage.vue';
export default { export default {
name: "CreateRoomAvatar", name: "CreateRoomAvatar",
components: { AuthedImage },
model: { model: {
prop: "modelValue", prop: "modelValue",
event: "update:modelValue", event: "update:modelValue",

View file

@ -106,7 +106,7 @@
<script> <script>
import messageMixin from "../messages/messageMixin"; import messageMixin from "../messages/messageMixin";
import sendAttachmentsMixin from "../sendAttachmentsMixin"; import sendAttachmentsMixin from "../sendAttachmentsMixin";
const prettyBytes = require("pretty-bytes"); import prettyBytes from "pretty-bytes";
export default { export default {
mixins: [messageMixin, sendAttachmentsMixin], mixins: [messageMixin, sendAttachmentsMixin],

View file

@ -95,11 +95,11 @@ export default {
methods: { methods: {
downloadOne() { downloadOne() {
if (this.currentItemIndex >= 0 && this.currentItemIndex < this.items.length) { if (this.currentItemIndex >= 0 && this.currentItemIndex < this.items.length) {
util.download(this.$matrix.matrixClient, this.items[this.currentItemIndex].event); util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.items[this.currentItemIndex].event);
} }
}, },
downloadAll() { downloadAll() {
this.items.forEach(item => util.download(this.$matrix.matrixClient, item.event)); this.items.forEach(item => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, item.event));
} }
} }
}; };

View file

@ -1,10 +1,12 @@
import defaultLogo from "@/assets/logo.svg";
export default { export default {
computed: { computed: {
logotype() { logotype() {
if (this.$config.logo) { if (this.$config.logo) {
return this.$config.logo; return this.$config.logo;
} }
return require("@/assets/logo.svg"); return defaultLogo;
} }
} }
} }

View file

@ -50,7 +50,7 @@ export default {
const width = this.$refs.image.$el.clientWidth; const width = this.$refs.image.$el.clientWidth;
const height = (width * 9) / 16; const height = (width * 9) / 16;
util util
.getThumbnail(this.$matrix.matrixClient, this.event, this.$config, width, height) .getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.event, this.$config, width, height)
.then((url) => { .then((url) => {
const info = this.event.getContent().info; const info = this.event.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to // JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to

View file

@ -88,7 +88,7 @@ export default {
}; };
ret.promise = ret.promise =
util util
.getThumbnail(this.$matrix.matrixClient, e, this.$config, 100, 100) .getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100)
.then((url) => { .then((url) => {
ret.src = url; ret.src = url;
}) })
@ -130,7 +130,7 @@ export default {
return rows return rows
}, },
downloadAll() { downloadAll() {
this.items.forEach(item => util.download(this.$matrix.matrixClient, item.event)); this.items.forEach(item => util.download(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, item.event));
} }
} }
}; };

View file

@ -26,7 +26,7 @@
color="#ededed" color="#ededed"
@click.stop="ownAvatarClicked" @click.stop="ownAvatarClicked"
> >
<img v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text headline">{{ userAvatarLetter }}</span> <span v-else class="white--text headline">{{ userAvatarLetter }}</span>
</v-avatar> </v-avatar>
<QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/> <QuickReactionsChannel v-if="room.displayType == ROOM_TYPE_CHANNEL" :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
@ -41,10 +41,11 @@ import messageMixin from "./messageMixin";
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils"; import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
import QuickReactions from "./QuickReactions.vue"; import QuickReactions from "./QuickReactions.vue";
import QuickReactionsChannel from "./channel/QuickReactionsChannel.vue"; import QuickReactionsChannel from "./channel/QuickReactionsChannel.vue";
import AuthedImage from "../AuthedImage.vue";
export default { export default {
mixins: [messageMixin], mixins: [messageMixin],
components: { QuickReactions, QuickReactionsChannel, SeenBy }, components: { QuickReactions, QuickReactionsChannel, SeenBy, AuthedImage },
data() { data() {
return { ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL } return { ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL }
}, },

View file

@ -49,7 +49,7 @@ export default {
const width = this.$refs.image.$el.clientWidth; const width = this.$refs.image.$el.clientWidth;
const height = (width * 9) / 16; const height = (width * 9) / 16;
util util
.getThumbnail(this.$matrix.matrixClient, this.event, this.$config, width, height) .getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, this.event, this.$config, width, height)
.then((url) => { .then((url) => {
const info = this.event.getContent().info; const info = this.event.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to // JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to

View file

@ -89,7 +89,7 @@ export default {
}; };
ret.promise = ret.promise =
util util
.getThumbnail(this.$matrix.matrixClient, e, this.$config, 100, 100) .getThumbnail(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, e, this.$config, 100, 100)
.then((url) => { .then((url) => {
ret.src = url; ret.src = url;
}) })

View file

@ -8,7 +8,7 @@
<transition-group name="list" tag="div" v-if="seenBy.length > 0"> <transition-group name="list" tag="div" v-if="seenBy.length > 0">
<v-avatar v-for="(member, index) in seenBy" :key="member.roomMember.userId" class="seen-by-user" size="16" color="grey" <v-avatar v-for="(member, index) in seenBy" :key="member.roomMember.userId" class="seen-by-user" size="16" color="grey"
v-show="index < SHOW_LIMIT" @click="open"> v-show="index < SHOW_LIMIT" @click="open">
<img v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" /> <AuthedImage v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
member.roomMember.name.substring(0, 1).toUpperCase() member.roomMember.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -28,7 +28,7 @@
<v-list-item v-for="(member, index) in seenBy" :key="index"> <v-list-item v-for="(member, index) in seenBy" :key="index">
<v-list-item-icon> <v-list-item-icon>
<v-avatar size="40" color="grey"> <v-avatar size="40" color="grey">
<img v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" /> <AuthedImage v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" />
<span v-else class="white--text headline">{{ <span v-else class="white--text headline">{{
member.roomMember.name.substring(0, 1).toUpperCase() member.roomMember.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
@ -45,12 +45,14 @@
</template> </template>
<script> <script>
import BottomSheet from "../BottomSheet.vue" import BottomSheet from "../BottomSheet.vue";
import AuthedImage from "../AuthedImage.vue";
import utils from "../../plugins/utils.js"; import utils from "../../plugins/utils.js";
export default { export default {
components: { components: {
BottomSheet BottomSheet,
AuthedImage
}, },
props: { props: {
room: { room: {
@ -114,7 +116,9 @@ export default {
16, 16,
16, 16,
"scale", "scale",
true true,
false,
this.$matrix.useAuthedMedia
); );
} }
return null; return null;

View file

@ -44,7 +44,7 @@ export default {
return; return;
} }
util util
.getAttachment(this.$matrix.matrixClient, event, (progress) => { .getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, event, (progress) => {
this.downloadProgress = progress; this.downloadProgress = progress;
console.log("Progress: " + progress); console.log("Progress: " + progress);
}) })

View file

@ -200,7 +200,7 @@ export default {
if (!this.$matrix.userAvatar) { if (!this.$matrix.userAvatar) {
return null; return null;
} }
return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true); return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true, undefined, this.$matrix.useAuthedMedia);
}, },
userAvatarLetter() { userAvatarLetter() {
@ -268,7 +268,7 @@ export default {
if (this.room) { if (this.room) {
const member = this.room.getMember(event.getSender()); const member = this.room.getMember(event.getSender());
if (member) { if (member) {
return member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true); return member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true, false, this.$matrix.useAuthedMedia);
} }
} }
return null; return null;

View file

@ -23,7 +23,7 @@ export default {
if (!this.$matrix.userAvatar) { if (!this.$matrix.userAvatar) {
return null; return null;
} }
return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, 'scale', true); return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, 'scale', true, undefined, this.$matrix.useAuthedMedia);
}, },
userAvatarLetter() { userAvatarLetter() {

View file

@ -140,7 +140,9 @@ export default {
40, 40,
40, 40,
"scale", "scale",
true true,
false,
this.$matrix.useAuthedMedia
); );
} }
return null; return null;
@ -243,7 +245,7 @@ export default {
privatePartyAvatar(size) { privatePartyAvatar(size) {
const other = this.privateParty; const other = this.privateParty;
if (other) { if (other) {
return other.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), size, size, "scale", true); return other.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), size, size, "scale", true, false, this.$matrix.useAuthedMedia);
} }
return undefined; return undefined;
}, },

View file

@ -10,7 +10,6 @@ import analytics from './services/analytics.service'
import audioPlayer from './services/audio.service'; import audioPlayer from './services/audio.service';
import 'roboto-fontface/css/roboto/roboto-fontface.css' import 'roboto-fontface/css/roboto/roboto-fontface.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css' import 'material-design-icons-iconfont/dist/material-design-icons.css'
import VEmojiPicker from 'v-emoji-picker';
import VueResize from 'vue-resize'; import VueResize from 'vue-resize';
import 'vue-resize/dist/vue-resize.css'; import 'vue-resize/dist/vue-resize.css';
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
@ -25,7 +24,6 @@ Vue.use(VueSanitize, defaultOptions);
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.use(VueResize); Vue.use(VueResize);
Vue.use(VEmojiPicker);
Vue.use(matrix, { store: store, i18n: i18n }); Vue.use(matrix, { store: store, i18n: i18n });
const configLoadedPromise = new Promise((resolve, ignoredreject) => { const configLoadedPromise = new Promise((resolve, ignoredreject) => {

View file

@ -5,15 +5,13 @@ Vue.use(VueI18n)
var messages = {} var messages = {}
function importAll(r) { const modules = import.meta.glob('@/assets/translations/*.json', {eager: true});
return r.keys().map(res => { Object.keys(modules).map(path => {
// Remove"./" // Remove"./"
const parts = res.split("/"); const parts = path.split("/");
const locale = parts[1].split(".")[0]; const locale = parts[parts.length - 1].split(".")[0];
messages[locale] = r(res); messages[locale] = modules[path];
}); });
}
importAll(require.context('@/assets/translations/', true, /\.json$/));
const vue18n = new VueI18n({ const vue18n = new VueI18n({
locale: 'en', locale: 'en',

View file

@ -4,9 +4,18 @@ import dataUriToBuffer from "data-uri-to-buffer";
import ImageResize from "image-resize"; import ImageResize from "image-resize";
import { AutoDiscovery } from 'matrix-js-sdk'; import { AutoDiscovery } from 'matrix-js-sdk';
import User from '../models/user'; import User from '../models/user';
const prettyBytes = require("pretty-bytes"); import prettyBytes from "pretty-bytes";
import Hammer from "hammerjs"; import Hammer from "hammerjs";
import { Thread } from 'matrix-js-sdk/lib/models/thread'; import { Thread } from 'matrix-js-sdk/lib/models/thread';
import sizeOf from "image-size";
import dayjs from "dayjs";
import jssha256 from "js-sha256";
import aesjs from "aes-js";
import { encode, decode } from 'json-web-key/lib/base64url';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import duration from 'dayjs/plugin/duration';
import { Buffer } from 'buffer/'
window.Buffer = Buffer;
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice"; export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted"; export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
@ -18,17 +27,11 @@ export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";
export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type"; export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";
const sizeOf = require("image-size");
var dayjs = require('dayjs'); var sha256 = jssha256.sha256;
var sha256 = require('js-sha256').sha256;
var aesjs = require('aes-js');
var base64Url = require('json-web-key/lib/base64url');
// Install extended localized format // Install extended localized format
var localizedFormat = require('dayjs/plugin/localizedFormat')
dayjs.extend(localizedFormat) dayjs.extend(localizedFormat)
var duration = require('dayjs/plugin/duration')
dayjs.extend(duration); dayjs.extend(duration);
// Store info about getUserMedia BEFORE we aply polyfill(s)! // Store info about getUserMedia BEFORE we aply polyfill(s)!
@ -68,9 +71,8 @@ class UploadPromise {
} }
class Util { class Util {
threadMessageType() { threadMessageType() {
return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread" return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
} }
getAttachmentUrlAndDuration(event) { getAttachmentUrlAndDuration(event) {
@ -88,45 +90,49 @@ class Util {
}); });
} }
getAttachment(matrixClient, event, progressCallback, asBlob = false, abortController = undefined) { getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const content = event.getContent(); const content = event.getContent();
if (content.url != null) {
// Unencrypted, just return!
resolve(matrixClient.mxcUrlToHttp(content.url));
return;
}
var url = null; var url = null;
var file = null; var file = null;
if (content.file && content.file.url) { let decrypt = true;
if (content.url != null) {
url = matrixClient.mxcUrlToHttp(content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
decrypt = false;
} else if (content.file && content.file.url) {
file = content.file; file = content.file;
url = matrixClient.mxcUrlToHttp(file.url); url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
} }
if (url == null) { if (url == null) {
reject("No url found!"); reject("No url found!");
} }
axios.get(url, { axios
.get(url, {
headers: {
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
},
signal: abortController ? abortController.signal : undefined, signal: abortController ? abortController.signal : undefined,
responseType: 'arraybuffer', onDownloadProgress: progressEvent => { responseType: "arraybuffer",
onDownloadProgress: (progressEvent) => {
let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total); let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
if (progressCallback) { if (progressCallback) {
progressCallback(percentCompleted); progressCallback(percentCompleted);
} }
} },
}) })
.then(response => { .then((response) => {
return this.decryptIfNeeded(file, response); return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({buffer:response.data});
}) })
.then(bytes => { .then((bytes) => {
if (asBlob) { if (asBlob) {
resolve(new Blob([bytes.buffer], { type: file.mimetype })); resolve(new Blob([bytes.buffer], { type: file.mimetype }));
} else { } else {
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype }))); resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
} }
}) })
.catch(err => { .catch((err) => {
console.log("Download error: ", err); console.log("Download error: ", err);
reject(err); reject(err);
}) })
@ -138,22 +144,16 @@ class Util {
}); });
} }
getThumbnail(matrixClient, event, config, ignoredw, ignoredh) { getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const content = event.getContent(); const content = event.getContent();
if (content.url != null) {
// Unencrypted, just return!
resolve(matrixClient.mxcUrlToHttp(content.url));
return;
}
var url = null; var url = null;
var file = null; var file = null;
if ( let decrypt = true;
content && if (content.url != null) {
content.info && url = matrixClient.mxcUrlToHttp(content.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
content.info.thumbnail_file && decrypt = false;
content.info.thumbnail_file.url } else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
) {
file = content.info.thumbnail_file; file = content.info.thumbnail_file;
// var width = w; // var width = w;
// var height = h; // var height = h;
@ -167,25 +167,34 @@ class Util {
// "scale", // "scale",
// true // true
// ); // );
url = matrixClient.mxcUrlToHttp(file.url); url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
} else if (content.file && content.file.url && this.getFileSize(event) > 0 && this.getFileSize(event) < config.maxSizeAutoDownloads) { } else if (
content.file &&
content.file.url &&
this.getFileSize(event) > 0 &&
this.getFileSize(event) < config.maxSizeAutoDownloads
) {
// No thumb, use real url // No thumb, use real url
file = content.file; file = content.file;
url = matrixClient.mxcUrlToHttp(file.url); url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
} }
if (url == null) { if (url == null) {
reject("No url found!"); reject("No url found!");
return; return;
} }
axios.get(url, { responseType: 'arraybuffer' })
.then(response => { axios
return this.decryptIfNeeded(file, response); .get(url, { responseType: "arraybuffer", headers: {
Authorization: `Bearer ${matrixClient.getAccessToken()}`,
}})
.then((response) => {
return decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({buffer:response.data});
}) })
.then(bytes => { .then((bytes) => {
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype }))); resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
}) })
.catch(err => { .catch((err) => {
console.log("Download error: ", err); console.log("Download error: ", err);
reject(err); reject(err);
}); });
@ -194,15 +203,18 @@ class Util {
decryptIfNeeded(file, response) { decryptIfNeeded(file, response) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var key = base64Url.decode(file.key.k); var key = decode(file.key.k);
var iv = base64Url.decode(file.iv); var iv = decode(file.iv);
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
const data = new Uint8Array(response.data); const data = new Uint8Array(response.data);
// const areEqual = (first, second) =>
// first.length === second.length && first.every((value, index) => value === second[index]);
// Calculate sha256 and compare hashes // Calculate sha256 and compare hashes
var hash = new Uint8Array(sha256.create().update(data).arrayBuffer()); var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
const originalHash = base64Url.decode(file.hashes.sha256); const originalHash = decode(file.hashes.sha256);
if (Buffer.compare(Buffer.from(hash), Buffer.from(originalHash.buffer)) != 0) { if (Buffer.compare(Buffer.from(hash), Buffer.from(originalHash.buffer)) != 0) {
reject("Hashes don't match!"); reject("Hashes don't match!");
return; return;
@ -216,27 +228,32 @@ class Util {
sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent) { sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent) {
var content = ContentHelpers.makeTextMessage(text); var content = ContentHelpers.makeTextMessage(text);
if (editedEvent) { if (editedEvent) {
content['m.relates_to'] = { content["m.relates_to"] = {
rel_type: 'm.replace', rel_type: "m.replace",
event_id: editedEvent.getId() event_id: editedEvent.getId(),
} };
content['m.new_content'] = { content["m.new_content"] = {
body: content.body, body: content.body,
msgtype: content.msgtype msgtype: content.msgtype,
} };
} else if (replyToEvent) { } else if (replyToEvent) {
content['m.relates_to'] = { content["m.relates_to"] = {
'm.in_reply_to': { "m.in_reply_to": {
event_id: replyToEvent.getId() event_id: replyToEvent.getId(),
} },
} };
let senderContent = replyToEvent.getContent() let senderContent = replyToEvent.getContent();
const senderContentBody = Object.getOwnPropertyDescriptor(senderContent, 'body') ? senderContent.body : Object.values(senderContent)[0].question.body const senderContentBody = Object.getOwnPropertyDescriptor(senderContent, "body")
? senderContent.body
: Object.values(senderContent)[0].question.body;
// Prefix the content with reply info (seems to be a legacy thing) // Prefix the content with reply info (seems to be a legacy thing)
const prefix = senderContentBody.split('\n').map((item, index) => { const prefix = senderContentBody
return "> " + (index == 0 ? ("<" + replyToEvent.getSender() + "> ") : "") + item; .split("\n")
}).join('\n'); .map((item, index) => {
return "> " + (index == 0 ? "<" + replyToEvent.getSender() + "> " : "") + item;
})
.join("\n");
content.body = prefix + "\n\n" + content.body; content.body = prefix + "\n\n" + content.body;
} }
return this.sendMessage(matrixClient, roomId, "m.room.message", content); return this.sendMessage(matrixClient, roomId, "m.room.message", content);
@ -244,30 +261,30 @@ class Util {
sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) { sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
const content = { const content = {
'm.relates_to': Object.assign(extraData, { "m.relates_to": Object.assign(extraData, {
key: emoji, key: emoji,
rel_type: 'm.annotation', rel_type: "m.annotation",
event_id: event.getId() event_id: event.getId(),
}) }),
}; };
return this.sendMessage(matrixClient, roomId, "m.reaction", content); return this.sendMessage(matrixClient, roomId, "m.reaction", content);
} }
createPoll(matrixClient, roomId, question, answers, isDisclosed) { createPoll(matrixClient, roomId, question, answers, isDisclosed) {
var idx = 0; var idx = 0;
let answerData = answers.map(a => { let answerData = answers.map((a) => {
idx++; idx++;
return { id: "" + idx, 'org.matrix.msc1767.text': a.text } return { id: "" + idx, "org.matrix.msc1767.text": a.text };
}); });
const content = { const content = {
'org.matrix.msc3381.poll.start': { "org.matrix.msc3381.poll.start": {
question: { question: {
'org.matrix.msc1767.text': question, "org.matrix.msc1767.text": question,
body: question body: question,
}, },
kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed", kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed",
max_selections: 1, max_selections: 1,
answers: answerData answers: answerData,
}, },
}; };
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content); return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content);
@ -275,24 +292,23 @@ class Util {
closePoll(matrixClient, roomId, event) { closePoll(matrixClient, roomId, event) {
const content = { const content = {
'm.relates_to': { "m.relates_to": {
rel_type: 'm.reference', rel_type: "m.reference",
event_id: event.getId() event_id: event.getId(),
},
'org.matrix.msc3381.poll.end': {
}, },
"org.matrix.msc3381.poll.end": {},
}; };
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content); return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content);
} }
sendPollAnswer(matrixClient, roomId, answers, event) { sendPollAnswer(matrixClient, roomId, answers, event) {
const content = { const content = {
'm.relates_to': { "m.relates_to": {
rel_type: 'm.reference', rel_type: "m.reference",
event_id: event.getId() event_id: event.getId(),
}, },
'org.matrix.msc3381.poll.response': { "org.matrix.msc3381.poll.response": {
answers: answers answers: answers,
}, },
}; };
return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content); return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
@ -300,12 +316,13 @@ class Util {
sendMessage(matrixClient, roomId, eventType, content) { sendMessage(matrixClient, roomId, eventType, content) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
matrixClient.sendEvent(roomId, eventType, content, undefined, undefined) matrixClient
.sendEvent(roomId, eventType, content, undefined, undefined)
.then((result) => { .then((result) => {
console.log("Message sent: ", result); console.log("Message sent: ", result);
resolve(result.event_id); resolve(result.event_id);
}) })
.catch(err => { .catch((err) => {
console.log("Send error: ", err); console.log("Send error: ", err);
if (err && err.name == "UnknownDeviceError") { if (err && err.name == "UnknownDeviceError") {
console.log("Unknown devices. Mark as known before retrying."); console.log("Unknown devices. Mark as known before retrying.");
@ -315,31 +332,28 @@ class Util {
for (var deviceId of Object.keys(userDevices)) { for (var deviceId of Object.keys(userDevices)) {
const deviceInfo = userDevices[deviceId]; const deviceInfo = userDevices[deviceId];
if (!deviceInfo.known) { if (!deviceInfo.known) {
setAsKnownPromises.push( setAsKnownPromises.push(matrixClient.setDeviceKnown(user, deviceId, true));
matrixClient.setDeviceKnown(
user,
deviceId,
true
)
);
} }
} }
} }
Promise.all(setAsKnownPromises) Promise.all(setAsKnownPromises).then(() => {
.then(() => {
// All devices now marked as "known", try to resend // All devices now marked as "known", try to resend
let event = err.event; let event = err.event;
if (!event) { if (!event) {
// Seems event is no longer send in the UnknownDevices error... // Seems event is no longer send in the UnknownDevices error...
const room = matrixClient.getRoom(roomId); const room = matrixClient.getRoom(roomId);
if (room) { if (room) {
event = room.getLiveTimeline().getEvents().find(e => { event = room
.getLiveTimeline()
.getEvents()
.find((e) => {
// Find the exact match (= object equality) // Find the exact match (= object equality)
return e.error === err return e.error === err;
}); });
} }
} }
matrixClient.resendEvent(event, matrixClient.getRoom(event.getRoomId())) matrixClient
.resendEvent(event, matrixClient.getRoom(event.getRoomId()))
.then((result) => { .then((result) => {
console.log("Message sent: ", result); console.log("Message sent: ", result);
resolve(result.event_id); resolve(result.event_id);
@ -349,8 +363,7 @@ class Util {
reject(err.toLocaleString()); reject(err.toLocaleString());
}); });
}); });
} } else {
else {
reject(err.toLocaleString()); reject(err.toLocaleString());
} }
}); });
@ -371,7 +384,7 @@ class Util {
const info = { const info = {
mimetype: file.type, mimetype: file.type,
size: file.size size: file.size,
}; };
// If audio, send duration in ms as well // If audio, send duration in ms as well
@ -380,38 +393,38 @@ class Util {
} }
var description = file.name; var description = file.name;
var msgtype = 'm.file'; var msgtype = "m.file";
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
msgtype = 'm.image'; msgtype = "m.image";
} else if (file.type.startsWith("audio/")) { } else if (file.type.startsWith("audio/")) {
msgtype = 'm.audio'; msgtype = "m.audio";
} else if (file.type.startsWith("video/")) { } else if (file.type.startsWith("video/")) {
msgtype = 'm.video'; msgtype = "m.video";
} }
const opts = { const opts = {
type: file.type, type: file.type,
name: description, name: description,
progressHandler: onUploadProgress, progressHandler: onUploadProgress,
onlyContentUri: false onlyContentUri: false,
}; };
var messageContent = { var messageContent = {
body: description, body: description,
info: info, info: info,
msgtype: msgtype msgtype: msgtype,
} };
// If thread root (an eventId) is set, add that here // If thread root (an eventId) is set, add that here
if (threadRoot) { if (threadRoot) {
messageContent["m.relates_to"] = { messageContent["m.relates_to"] = {
"rel_type": this.threadMessageType(), rel_type: this.threadMessageType(),
"event_id": threadRoot event_id: threadRoot,
}; };
} }
// Set filename for files // Set filename for files
if (msgtype == 'm.file') { if (msgtype == "m.file") {
messageContent.filename = file.name; messageContent.filename = file.name;
} }
@ -424,23 +437,22 @@ class Util {
promise promise
.then((response) => { .then((response) => {
messageContent.url = response.content_uri; messageContent.url = response.content_uri;
return (msgtype == 'm.audio' ? this.generateWaveform(fileContents, messageContent) : true); return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true;
}) })
.then(() => { .then(() => {
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent) return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
}) })
.then(result => { .then((result) => {
resolve(result); resolve(result);
}) })
.catch(err => { .catch((err) => {
reject(err); reject(err);
}); });
return; // Don't fall through return; // Don't fall through
} }
const crypto = require('crypto'); let key = Buffer.from(crypto.getRandomValues(new Uint8Array(256 / 8)));
let key = crypto.randomBytes(256 / 8); let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector.
let iv = Buffer.concat([crypto.randomBytes(8), Buffer.alloc(8)]); // Initialization vector.
// Encrypt // Encrypt
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
@ -451,19 +463,19 @@ class Util {
var hash = new Uint8Array(sha256.create().update(data).arrayBuffer()); var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
const jwk = { const jwk = {
kty: 'oct', kty: "oct",
key_ops: ['encrypt', 'decrypt'], key_ops: ["encrypt", "decrypt"],
alg: 'A256CTR', alg: "A256CTR",
k: base64Url.encode(key), k: encode(key),
ext: true ext: true,
}; };
const encryptedFile = { const encryptedFile = {
mimetype: file.type, mimetype: file.type,
key: jwk, key: jwk,
iv: Buffer.from(iv).toString('base64').replace(/=/g, ''), iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
hashes: { sha256: Buffer.from(hash).toString('base64').replace(/=/g, '') }, hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
v: 'v2' v: "v2",
}; };
messageContent.file = encryptedFile; messageContent.file = encryptedFile;
@ -481,21 +493,21 @@ class Util {
return reject(response.error); return reject(response.error);
} }
encryptedFile.url = response.content_uri; encryptedFile.url = response.content_uri;
return (msgtype == 'm.audio' ? this.generateWaveform(fileContents, messageContent) : true); return msgtype == "m.audio" ? this.generateWaveform(fileContents, messageContent) : true;
}) })
.then(() => { .then(() => {
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent) return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
}) })
.then(result => { .then((result) => {
resolve(result); resolve(result);
}) })
.catch(err => { .catch((err) => {
reject(err); reject(err);
}); });
} };
reader.onerror = (err) => { reader.onerror = (err) => {
reject(err); reject(err);
} };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}); });
return uploadPromise; return uploadPromise;
@ -507,8 +519,7 @@ class Util {
} }
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx) { if (audioCtx) {
return audioCtx.decodeAudioData(data) return audioCtx.decodeAudioData(data).then((audioBuffer) => {
.then((audioBuffer) => {
const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel
const samples = 1000; // Number of samples const samples = 1000; // Number of samples
const blockSize = Math.floor(rawData.length / samples); const blockSize = Math.floor(rawData.length / samples);
@ -517,18 +528,17 @@ class Util {
let blockStart = blockSize * i; // the location of the first sample in the block let blockStart = blockSize * i; // the location of the first sample in the block
let sum = 0; let sum = 0;
for (let j = 0; j < blockSize; j++) { for (let j = 0; j < blockSize; j++) {
sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
} }
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
} }
// Normalize // Normalize
const multiplier = Math.pow(Math.max(...filteredData), -1); const multiplier = Math.pow(Math.max(...filteredData), -1);
filteredData = filteredData.map(n => n * multiplier); filteredData = filteredData.map((n) => n * multiplier);
// Integerize // Integerize
filteredData = filteredData.map(n => parseInt((n * 255).toFixed())); filteredData = filteredData.map((n) => parseInt((n * 255).toFixed()));
// Generate SVG of waveform // Generate SVG of waveform
let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`; let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`;
@ -542,7 +552,7 @@ class Util {
messageContent.format = "org.matrix.custom.html"; messageContent.format = "org.matrix.custom.html";
messageContent.formatted_body = svg; messageContent.formatted_body = svg;
}) });
} }
} }
@ -596,24 +606,18 @@ class Util {
/** Generate a random user name */ /** Generate a random user name */
randomUser(prefix) { randomUser(prefix) {
var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, '') : null; var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, "") : null;
if (!pfx || pfx.length == 0) { if (!pfx || pfx.length == 0) {
pfx = "weblite-"; pfx = "weblite-";
} }
return pfx + this.randomString( return pfx + this.randomString(12, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
12,
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
);
} }
/** /**
* Generate random 12 char password * Generate random 12 char password
*/ */
randomPass() { randomPass() {
return this.randomString( return this.randomString(12, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#_-*+");
12,
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#_-*+"
);
} }
// From here: https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript // From here: https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
@ -621,9 +625,7 @@ class Util {
var result = ""; var result = "";
var charactersLength = characters.length; var charactersLength = characters.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
result += characters.charAt( result += characters.charAt(Math.floor(Math.random() * charactersLength));
Math.floor(Math.random() * charactersLength)
);
} }
return result; return result;
} }
@ -690,16 +692,18 @@ class Util {
isChildVisible(parentNode, childNode) { isChildVisible(parentNode, childNode) {
const rect1 = parentNode.getBoundingClientRect(); const rect1 = parentNode.getBoundingClientRect();
const rect2 = childNode.getBoundingClientRect(); const rect2 = childNode.getBoundingClientRect();
var overlap = !(rect1.right <= rect2.left || var overlap = !(
rect1.right <= rect2.left ||
rect1.left >= rect2.right || rect1.left >= rect2.right ||
rect1.bottom <= rect2.top || rect1.bottom <= rect2.top ||
rect1.top >= rect2.bottom) rect1.top >= rect2.bottom
);
return overlap; return overlap;
} }
findOneVisibleElement(parentNode) { findOneVisibleElement(parentNode) {
let start = 0; let start = 0;
let end = (parentNode && parentNode.children) ? parentNode.children.length - 1 : -1; let end = parentNode && parentNode.children ? parentNode.children.length - 1 : -1;
while (start <= end) { while (start <= end) {
let middle = Math.floor((start + end) / 2); let middle = Math.floor((start + end) / 2);
let childNode = parentNode.children[middle]; let childNode = parentNode.children[middle];
@ -720,7 +724,7 @@ class Util {
_importAll(r) { _importAll(r) {
var images = []; var images = [];
r.keys().forEach(res => { r.keys().forEach((res) => {
console.log("Avatar", res); console.log("Avatar", res);
// // Remove"./" // // Remove"./"
var name = res.split("_")[1]; var name = res.split("_")[1];
@ -734,38 +738,49 @@ class Util {
} }
getDefaultAvatars() { getDefaultAvatars() {
return this._importAll(require.context('../assets/avatars/', true, /\.(jpeg|jpg|png)$/)); var images = [];
const modules = import.meta.glob("../assets/avatars/*.(jpeg|jpg|png)", { eager: true });
Object.keys(modules).map((path) => {
var name = path.split("_")[1];
name = name.slice(0, name.indexOf("."));
name = name.charAt(0).toUpperCase() + name.slice(1);
const image = modules[path].default;
const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
images.push({ id: path, image: image, name: "Guest " + name + " " + randomNumber });
});
return images;
} }
setAvatar(matrix, file, onUploadProgress) { setAvatar(matrix, file, onUploadProgress) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get(file, { responseType: 'arraybuffer' }) axios
.then(response => { .get(file, { responseType: "arraybuffer" })
.then((response) => {
const opts = { const opts = {
type: response.headers['content-type'].split(';')[0], type: response.headers["content-type"].split(";")[0],
name: "Avatar", name: "Avatar",
progressHandler: onUploadProgress, progressHandler: onUploadProgress,
onlyContentUri: false onlyContentUri: false,
}; };
var avatarUri; var avatarUri;
matrix.matrixClient.uploadContent(response.data, opts) matrix.matrixClient
.uploadContent(response.data, opts)
.then((response) => { .then((response) => {
avatarUri = response.content_uri; avatarUri = response.content_uri;
return matrix.matrixClient.setAvatarUrl(avatarUri); return matrix.matrixClient.setAvatarUrl(avatarUri);
}) })
.then(result => { .then((result) => {
matrix.userAvatar = avatarUri; matrix.userAvatar = avatarUri;
resolve(result); resolve(result);
}) })
.catch(err => { .catch((err) => {
reject(err); reject(err);
}); });
}) })
.catch(err => { .catch((err) => {
reject(err); reject(err);
}); });
});
})
} }
setRoomAvatar(matrixClient, roomId, file, onUploadProgress) { setRoomAvatar(matrixClient, roomId, file, onUploadProgress) {
@ -777,36 +792,38 @@ class Util {
const info = { const info = {
mimetype: file.type, mimetype: file.type,
size: file.size size: file.size,
}; };
const opts = { const opts = {
type: file.type, type: file.type,
name: "Room Avatar", name: "Room Avatar",
progressHandler: onUploadProgress, progressHandler: onUploadProgress,
onlyContentUri: false onlyContentUri: false,
}; };
var messageContent = { var messageContent = {
body: file.name, body: file.name,
info: info info: info,
} };
matrixClient.uploadContent(data, opts) matrixClient
.uploadContent(data, opts)
.then((response) => { .then((response) => {
messageContent.url = response.content_uri; messageContent.url = response.content_uri;
return matrixClient.sendStateEvent(roomId, "m.room.avatar", messageContent); return matrixClient.sendStateEvent(roomId, "m.room.avatar", messageContent);
}) })
.then(result => { .then((result) => {
resolve(result); resolve(matrixClient.mxcUrlToHttp(messageContent.url, 80, 80, "scale", undefined, undefined, true))
// resolve(result);
}) })
.catch(err => { .catch((err) => {
reject(err); reject(err);
}); });
} };
reader.onerror = (err) => { reader.onerror = (err) => {
reject(err); reject(err);
} };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}); });
} }
@ -828,9 +845,7 @@ class Util {
if (w > 640 || h > 640) { if (w > 640 || h > 640) {
var aspect = w / h; var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed()); var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
var newHeight = parseInt( var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
(w > h ? 640 / aspect : 640).toFixed()
);
var imageResize = new ImageResize({ var imageResize = new ImageResize({
format: "png", format: "png",
width: newWidth, width: newWidth,
@ -871,20 +886,20 @@ class Util {
* @param {*} ts2 * @param {*} ts2
*/ */
dayDiff(ts1, ts2) { dayDiff(ts1, ts2) {
var t1 = dayjs(ts1).endOf('day'); var t1 = dayjs(ts1).endOf("day");
var t2 = dayjs(ts2).endOf('day'); var t2 = dayjs(ts2).endOf("day");
return t2.diff(t1, 'day'); return t2.diff(t1, "day");
} }
dayDiffToday(timestamp) { dayDiffToday(timestamp) {
var then = dayjs(timestamp).endOf('day'); var then = dayjs(timestamp).endOf("day");
var now = dayjs().endOf('day'); var now = dayjs().endOf("day");
return now.diff(then, 'day'); return now.diff(then, "day");
} }
formatDay(timestamp) { formatDay(timestamp) {
var then = dayjs(timestamp).endOf('day'); var then = dayjs(timestamp).endOf("day");
return then.format('L'); return then.format("L");
} }
formatTime(time) { formatTime(time) {
@ -908,7 +923,7 @@ class Util {
} }
formatDuration(ms) { formatDuration(ms) {
if (ms >= (60 * 60000)) { if (ms >= 60 * 60000) {
return dayjs.duration(ms).format("H:mm:ss"); return dayjs.duration(ms).format("H:mm:ss");
} }
return dayjs.duration(ms).format("m:ss"); return dayjs.duration(ms).format("m:ss");
@ -916,7 +931,7 @@ class Util {
formatRecordStartTime(timestamp) { formatRecordStartTime(timestamp) {
var then = dayjs(timestamp); var then = dayjs(timestamp);
return then.format('lll'); return then.format("lll");
} }
browserCanRecordAudio() { browserCanRecordAudio() {
@ -924,8 +939,8 @@ class Util {
} }
getRoomNameFromAlias(alias) { getRoomNameFromAlias(alias) {
if (alias && alias.startsWith('#') && alias.indexOf(':') > 0) { if (alias && alias.startsWith("#") && alias.indexOf(":") > 0) {
return alias.slice(1).split(':')[0]; return alias.slice(1).split(":")[0];
} }
return undefined; return undefined;
} }
@ -934,8 +949,9 @@ class Util {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var preferredAlias = roomName.replace(/\s/g, "").toLowerCase(); var preferredAlias = roomName.replace(/\s/g, "").toLowerCase();
var tryAlias = "#" + preferredAlias + ":" + defaultMatrixDomainPart; var tryAlias = "#" + preferredAlias + ":" + defaultMatrixDomainPart;
matrixClient.getRoomIdForAlias(tryAlias) matrixClient
.then(ignoredid => { .getRoomIdForAlias(tryAlias)
.then((ignoredid) => {
// We got a response, this means the tryAlias already exists. // We got a response, this means the tryAlias already exists.
// Try again, with appended random chars // Try again, with appended random chars
if (iterationCount) { if (iterationCount) {
@ -948,26 +964,32 @@ class Util {
roomName = roomName.substring(0, roomName.length - 5); roomName = roomName.substring(0, roomName.length - 5);
} }
const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789"); const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789");
resolve(this.getUniqueAliasForRoomName(matrixClient, roomName + "-" + randomChars, defaultMatrixDomainPart, (iterationCount || 0) + 1)) resolve(
this.getUniqueAliasForRoomName(
matrixClient,
roomName + "-" + randomChars,
defaultMatrixDomainPart,
(iterationCount || 0) + 1
)
);
}) })
.catch(err => { .catch((err) => {
if (err.errcode == 'M_NOT_FOUND') { if (err.errcode == "M_NOT_FOUND") {
resolve(preferredAlias); resolve(preferredAlias);
} else { } else {
reject(err); reject(err);
} }
}) });
}); });
} }
downloadableTypes() { downloadableTypes() {
return ['m.video','m.audio','m.image','m.file']; return ["m.video", "m.audio", "m.image", "m.file"];
} }
download(matrixClient, event) { download(matrixClient, useAuthedMedia, event) {
console.log("DOWNLOAD"); console.log("DOWNLOAD");
this this.getAttachment(matrixClient, useAuthedMedia, event)
.getAttachment(matrixClient, event)
.then((url) => { .then((url) => {
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
@ -990,6 +1012,7 @@ class Util {
} }
getMatrixBaseUrl(user, config) { getMatrixBaseUrl(user, config) {
console.error("getMatrixBaseUrl", user, config);
if (user) { if (user) {
const domain = User.domainPart(user.user_id); const domain = User.domainPart(user.user_id);
if (domain) { if (domain) {
@ -1000,7 +1023,7 @@ class Util {
} }
return AutoDiscovery.findClientConfig(domain) return AutoDiscovery.findClientConfig(domain)
.then((clientConfig) => { .then((clientConfig) => {
const hs = clientConfig['m.homeserver']; const hs = clientConfig["m.homeserver"];
if (hs && !hs.error && hs.base_url) { if (hs && !hs.error && hs.base_url) {
console.log("Use home server returned from well-known", hs.base_url); console.log("Use home server returned from well-known", hs.base_url);
return hs.base_url; return hs.base_url;
@ -1019,7 +1042,11 @@ class Util {
getMimeType(event) { getMimeType(event) {
const content = event.getContent(); const content = event.getContent();
return (content.info && content.info.mimetype) ? content.info.mimetype : (content.file && content.file.mimetype) ? content.file.mimetype : ""; return content.info && content.info.mimetype
? content.info.mimetype
: content.file && content.file.mimetype
? content.file.mimetype
: "";
} }
getFileName(event) { getFileName(event) {
@ -1073,7 +1100,10 @@ class Util {
isFileTypeZip(event) { isFileTypeZip(event) {
const mime = this.getMimeType(event); const mime = this.getMimeType(event);
if (["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) || this.getFileName(event).endsWith(".zip")) { if (
["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) ||
this.getFileName(event).endsWith(".zip")
) {
return true; return true;
} }
return false; return false;
@ -1090,12 +1120,12 @@ class Util {
// reference: https://codepen.io/jtangelder/pen/xxYyJQ // reference: https://codepen.io/jtangelder/pen/xxYyJQ
const hm = new Hammer.Manager(element); const hm = new Hammer.Manager(element);
hm.add(new Hammer.Tap({ event: 'doubletap', taps: 2 })); hm.add(new Hammer.Tap({ event: "doubletap", taps: 2 }));
hm.add(new Hammer.Tap({ event: 'singletap' }) ); hm.add(new Hammer.Tap({ event: "singletap" }));
hm.get('doubletap').recognizeWith('singletap'); hm.get("doubletap").recognizeWith("singletap");
hm.get('singletap').requireFailure('doubletap'); hm.get("singletap").requireFailure("doubletap");
return hm return hm;
} }
} }
export default new Util(); export default new Util();

View file

@ -4,15 +4,14 @@ import Vuetify from 'vuetify/lib';
// Import all .vue icons and process them, so they can be used // Import all .vue icons and process them, so they can be used
// as $vuetify.icons.<iconname> // as $vuetify.icons.<iconname>
var icons = {} var icons = {}
function importAll(r) { const modules = import.meta.glob('@/assets/icons/*.vue', {eager: true});
return r.keys().map(res => { Object.keys(modules).map(path => {
// Remove"./" // Remove"./"
const parts = res.split("/"); const parts = path.split("/");
const iconName = parts[1].split(".")[0]; const iconName = parts[parts.length - 1].split(".")[0];
icons[iconName] = { component: r(res).default }; icons[iconName] = { component: modules[path].default }
}); });
}
importAll(require.context('@/assets/icons/', true, /\.vue$/));
Vue.use(Vuetify); Vue.use(Vuetify);

View file

@ -117,7 +117,7 @@ export default {
info.loading = true; info.loading = true;
info.abortController = new AbortController(); info.abortController = new AbortController();
utils utils
.getAttachment(this.$root.$matrix.matrixClient, event, (progress) => { .getAttachment(this.$root.$matrix.matrixClient, this.$root.$matrix.useAuthedMedia, event, (progress) => {
info.loadPercent = progress; info.loadPercent = progress;
}, false, info.abortController) }, false, info.abortController)
.then((url) => { .then((url) => {

View file

@ -1,6 +1,8 @@
import * as defaultConfig from "@/assets/config.json";
export default { export default {
install(Vue, defaultServerFromLocation, onloaded) { install(Vue, defaultServerFromLocation, onloaded) {
var config = Vue.observable(require('@/assets/config.json')); var config = Vue.observable(defaultConfig.default);
Vue.set(config, "loaded", false); Vue.set(config, "loaded", false);
const getRuntimeConfig = () => { const getRuntimeConfig = () => {
return fetch('./config.json?ms=' + Date.now()).then((res) => res.json()).catch(err => { return fetch('./config.json?ms=' + Date.now()).then((res) => res.json()).catch(err => {
@ -42,6 +44,8 @@ export default {
} }
Vue.set(config, "loaded", true); Vue.set(config, "loaded", true);
document.title = config.appName || "";
// Tell callback we are done loading runtime config // Tell callback we are done loading runtime config
if (onloaded) { if (onloaded) {
onloaded(config); onloaded(config);

View file

@ -4,9 +4,9 @@ import * as sdk from "matrix-js-sdk";
import { TimelineWindow, EventTimeline, EventStatus } from "matrix-js-sdk"; 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 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 User from "../models/user";
import * as LocalStorageCryptoStoreClass from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store";
const LocalStorageCryptoStore = const LocalStorageCryptoStore = LocalStorageCryptoStoreClass.LocalStorageCryptoStore;
require("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store").LocalStorageCryptoStore;
export const CHANNEL_POWER_LEVELS = { 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 "m.room.encrypted": 0, // NOTE! Since practically all events in encrypted rooms get sent as "m.room.encrypted" we need to set
@ -50,6 +50,7 @@ export default {
userCanSendReactionAndAnswerPollInCurrentRoom: true, userCanSendReactionAndAnswerPollInCurrentRoom: true,
currentRoomBeingPurged: false, currentRoomBeingPurged: false,
notificationCount: 0, notificationCount: 0,
useAuthedMedia: false,
}; };
}, },
mounted() { mounted() {
@ -297,14 +298,16 @@ export default {
accessToken: user.access_token, accessToken: user.access_token,
timelineSupport: true, timelineSupport: true,
unstableClientRelationAggregation: true, unstableClientRelationAggregation: true,
cryptoStore: this.createCryptoStore()
//useAuthorizationHeader: true //useAuthorizationHeader: true
}; };
this.matrixClient = sdk.createClient(opts); this.matrixClient = sdk.createClient(opts);
// if (user.is_guest) { // if (user.is_guest) {
// this.matrixClient.setGuest(true); // this.matrixClient.setGuest(true);
// } // }
console.error("Created client", this.matrixClient);
return this.matrixClient return this.matrixClient
.initCrypto() .initRustCrypto()
.then(() => { .then(() => {
console.log("Crypto initialized"); console.log("Crypto initialized");
@ -331,6 +334,11 @@ export default {
} }
}) })
.then(() => { .then(() => {
return this.matrixClient.isVersionSupported("v1.11");
})
.then((authedMediaSupported) => {
this.useAuthedMedia = authedMediaSupported;
// Ready to use! Start by loading rooms. // Ready to use! Start by loading rooms.
this.initClient(); this.initClient();
return user; return user;
@ -391,7 +399,7 @@ export default {
Vue.set( Vue.set(
room, room,
"avatar", "avatar",
room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true) room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia)
); );
} }
} }
@ -517,7 +525,7 @@ export default {
}); });
updatedRooms.forEach((room) => { updatedRooms.forEach((room) => {
if (!room.avatar) { if (!room.avatar) {
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true)); Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia));
} }
}); });
Vue.set(this, "rooms", updatedRooms); Vue.set(this, "rooms", updatedRooms);
@ -1208,34 +1216,6 @@ export default {
} }
}, },
getPublicUserInfo(userId) {
if (!userId) {
return Promise.reject("Invalid parameters");
}
const parts = userId.split(":");
if (parts.length != 2) {
return Promise.reject("Unknown home server");
}
const clientPromise = this.getPublicQueryMatrixClient();
let matrixClient;
return clientPromise
.then((client) => {
matrixClient = client;
return client.getProfileInfo(userId);
})
.then((response) => {
if (response.avatar_url) {
response.avatar = matrixClient.mxcUrlToHttp(response.avatar_url, 80, 80, "scale", true);
}
return Promise.resolve(response);
})
.catch((err) => {
return Promise.reject("Failed to find user info: " + err);
});
},
getPublicRoomInfo(roomId) { getPublicRoomInfo(roomId) {
if (!roomId) { if (!roomId) {
return Promise.reject("Invalid parameters"); return Promise.reject("Invalid parameters");
@ -1249,14 +1229,14 @@ export default {
const clientPromise = this.getPublicQueryMatrixClient(); const clientPromise = this.getPublicQueryMatrixClient();
const findOrGetMore = function _findOrGetMore(client, response) { const findOrGetMore = function _findOrGetMore(client, useAuthedMedia, response) {
for (var room of response.chunk) { for (var room of response.chunk) {
if ( if (
(roomId.startsWith("#") && room.canonical_alias == roomId) || (roomId.startsWith("#") && room.canonical_alias == roomId) ||
(roomId.startsWith("!") && room.room_id == roomId) (roomId.startsWith("!") && room.room_id == roomId)
) { ) {
if (room.avatar_url) { if (room.avatar_url) {
room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", true); room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", true, undefined, useAuthedMedia);
} }
return Promise.resolve(room); return Promise.resolve(room);
} }
@ -1280,13 +1260,18 @@ export default {
}; };
var matrixClient; var matrixClient;
let useAuthedMedia = false;
return clientPromise return clientPromise
.then((client) => { .then((client) => {
matrixClient = client; matrixClient = client;
return client.isVersionSupported("v1.11");
})
.then((version1_11) => {
useAuthedMedia = version1_11;
return matrixClient.publicRooms({ server: server, limit: 1000 }); return matrixClient.publicRooms({ server: server, limit: 1000 });
}) })
.then((response) => { .then((response) => {
return findOrGetMore(matrixClient, response); return findOrGetMore(matrixClient, useAuthedMedia, response);
}) })
.catch((err) => { .catch((err) => {
return Promise.reject("Failed to find room: " + err); return Promise.reject("Failed to find room: " + err);

53
vite.config.mjs Normal file
View file

@ -0,0 +1,53 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2";
import { fileURLToPath, URL } from "node:url";
import Components from "unplugin-vue-components/vite";
import { VuetifyResolver } from "unplugin-vue-components/resolvers";
import { viteStaticCopy } from 'vite-plugin-static-copy';
import nodePolyfills from 'rollup-plugin-polyfill-node';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [VuetifyResolver()],
}),
viteStaticCopy({
targets: [
{
src: "src/assets/config.json",
dest: ''
},
{
src: "node_modules/@matrix-org/olm/olm.wasm",
dest: ''
},
{
src: 'node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm',
dest: '/node_modules/.vite/deps/pkg'
}
]
}),
nodePolyfills()
],
resolve: {
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue", ".wasm"],
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"~vuetify": fileURLToPath(new URL("./node_modules/vuetify", import.meta.url)),
},
},
define: {
global: "window",
Lame: "window.Lame",
Presets: "window.Presets",
GainAnalysis: "window.GainAnalysis",
QuantizePVT: "window.QuantizePVT",
Quantize: "window.Quantize",
Takehiro: "window.Takehiro",
Reservoir: "window.Reservoir",
MPEGMode: "window.MPEGMode",
BitStream: "window.BitStream",
},
});