Start on Vue 3 changes

This commit is contained in:
N-Pex 2025-05-06 09:27:53 +02:00
parent dcc4784bfd
commit c913a40e18
35 changed files with 3570 additions and 1913 deletions

View file

@ -6,7 +6,7 @@
<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="/favicon.ico" /> <link rel="icon" id="favicon" href="/favicon.ico" />
<title></title> <title></title>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="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" />
<link rel="apple-touch-icon" href="./icons/icon-128x128.png" sizes="128x128" /> <link rel="apple-touch-icon" href="./icons/icon-128x128.png" sizes="128x128" />
@ -47,6 +47,7 @@
<lottie-player autoplay loop mode="normal" src="./loader.json" style="width: 128px"> </lottie-player> <lottie-player autoplay loop mode="normal" src="./loader.json" style="width: 128px"> </lottie-player>
</div> </div>
</div> </div>
<div id="app2"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>

2001
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@matrix-org/olm": "^3.2.12", "@matrix-org/olm": "^3.2.12",
"@vitejs/plugin-vue2": "^2.3.3", "@vitejs/plugin-vue": "^5.2.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",
@ -40,21 +40,23 @@
"roboto-fontface": "*", "roboto-fontface": "*",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"util": "^0.12.5", "util": "^0.12.5",
"v-emoji-picker": "^2.3.1", "vue": "^3.5.13",
"vue": "^2.7.16", "vue-3-sanitize": "^0.1.4",
"vue-clipboard2": "^0.3.3", "vue-clipboard2": "^0.3.3",
"vue-i18n": "^8.28.2", "vue-i18n": "^11.1.3",
"vue-resize": "^1.0", "vue-router": "^4.5.1",
"vue-router": "^3.6.5",
"vue-sanitize": "^0.2.3",
"vue-swipeable-bottom-sheet": "^0.0.5", "vue-swipeable-bottom-sheet": "^0.0.5",
"vuetify": "^2.7.2", "vue3-emoji-picker": "^1.1.8",
"vuex": "^3.5.1", "vue3-resize": "^0.2.0",
"vuetify": "^3.8.3",
"vuex": "^4.1.0",
"vuex-persist": "^3.1.3" "vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^28.0.3",
"@types/jszip": "^3.4.0", "@types/jszip": "^3.4.0",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vue/compiler-sfc": "^3.5.13",
"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",
@ -65,9 +67,9 @@
"unplugin-vue-components": "^28.4.1", "unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2", "vite": "^6.2.2",
"vite-plugin-static-copy": "^2.3.0", "vite-plugin-static-copy": "^2.3.0",
"vite-plugin-vuetify": "^2.1.1",
"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"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View file

@ -53,7 +53,7 @@
v-on:redact="redact(selectedEvent)" v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)" v-on:download="download(selectedEvent)"
v-on:more=" v-on:more="
isEmojiQuickReaction= true isEmojiQuickReaction=true;
showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor}) showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
" "
:userCanSendReactionAndAnswerPoll="$matrix.userCanSendReactionAndAnswerPollInCurrentRoom" :userCanSendReactionAndAnswerPoll="$matrix.userCanSendReactionAndAnswerPollInCurrentRoom"
@ -97,7 +97,7 @@
v-on:download="download(event)" v-on:download="download(event)"
v-on:poll-closed="pollWasClosed(event)" v-on:poll-closed="pollWasClosed(event)"
v-on:more=" v-on:more="
isEmojiQuickReaction = true isEmojiQuickReaction = true;
showMoreMessageOperations({event: event, anchor: $event.anchor}) showMoreMessageOperations({event: event, anchor: $event.anchor})
" "
v-on:layout-change="onLayoutChange" v-on:layout-change="onLayoutChange"
@ -205,7 +205,7 @@
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1 input-more-icon"> <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1 input-more-icon">
<v-btn fab small elevation="0" v-blur @click.stop=" <v-btn fab small elevation="0" v-blur @click.stop="
isEmojiQuickReaction = false isEmojiQuickReaction = false;
showMoreMessageOperations($event) showMoreMessageOperations($event)
"> ">
<v-icon>$vuetify.icons.addReaction</v-icon> <v-icon>$vuetify.icons.addReaction</v-icon>
@ -313,7 +313,7 @@
</div> </div>
<MessageOperationsBottomSheet ref="messageOperationsSheet"> <MessageOperationsBottomSheet ref="messageOperationsSheet">
<VEmojiPicker ref="emojiPicker" @select="emojiSelected" :i18n="i18nEmoji"/> <EmojiPicker ref="emojiPicker" @select="emojiSelected" :i18n="i18nEmoji"/>
</MessageOperationsBottomSheet> </MessageOperationsBottomSheet>
<StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" /> <StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />
@ -366,7 +366,6 @@
</template> </template>
<script> <script>
import Vue from "vue";
import { TimelineWindow, EventTimeline } from "matrix-js-sdk"; import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
import util, { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE, ROOM_TYPE_CHANNEL } from "../plugins/utils"; import util, { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE, ROOM_TYPE_CHANNEL } from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue"; import MessageOperations from "./messages/MessageOperations.vue";
@ -397,7 +396,7 @@ import MessageOperationsChannel from './messages/channel/MessageOperationsChanne
import { imageSize } from "image-size"; import { imageSize } from "image-size";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import RoomExport from "./RoomExport.vue"; import RoomExport from "./RoomExport.vue";
import { VEmojiPicker } from 'v-emoji-picker'; import EmojiPicker from 'vue3-emoji-picker';
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 */
@ -452,7 +451,7 @@ export default {
MessageErrorHandler, MessageErrorHandler,
MessageOperationsChannel, MessageOperationsChannel,
RoomExport, RoomExport,
VEmojiPicker EmojiPicker
}, },
data() { data() {
@ -755,17 +754,17 @@ export default {
let lastDisplayedEvent = undefined; let lastDisplayedEvent = undefined;
events = events.flatMap((e) => { events = events.flatMap((e) => {
let result = []; let result = [];
Vue.set(e, "component", this.componentForEvent(e, false)); e.component = this.componentForEvent(e, false);
if (e.getId() == this.readMarker && showReadMarker) { if (e.getId() == this.readMarker && showReadMarker) {
const readMarkerEvent = ROOM_READ_MARKER_EVENT_PLACEHOLDER; const readMarkerEvent = ROOM_READ_MARKER_EVENT_PLACEHOLDER;
Vue.set(readMarkerEvent, "component", this.componentForEvent(readMarkerEvent, false)); readMarkerEvent["component"] = this.componentForEvent(readMarkerEvent, false);
if (readMarkerEvent.component) { if (readMarkerEvent.component) {
Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent); e["nextDisplayedEvent"] = lastDisplayedEvent;
} }
result.push(readMarkerEvent); result.push(readMarkerEvent);
} }
if (e.component) { if (e.component) {
Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent); e["nextDisplayedEvent"] = lastDisplayedEvent;
lastDisplayedEvent = e; lastDisplayedEvent = e;
if (e.getSender() !== this.$matrix.currentUserId) { if (e.getSender() !== this.$matrix.currentUserId) {
showReadMarker = true; showReadMarker = true;
@ -1006,8 +1005,8 @@ export default {
if (this.room) { if (this.room) {
const pinnedEvents = this.$matrix.getPinnedEvents(this.room); const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
updated.forEach((e) => { updated.forEach((e) => {
Vue.set(e, "isPinned", pinnedEvents.includes(e.threadParent ? e.threadParent.getId() : e.getId())); e["isPinned"] = pinnedEvents.includes(e.threadParent ? e.threadParent.getId() : e.getId());
Vue.set(e, "isChannelMessage", (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL)); e["isChannelMessage"] = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL);
}); });
updated = updated.sort((e1, e2) => { updated = updated.sort((e1, e2) => {
@ -1080,7 +1079,7 @@ export default {
} }
this.reverseOrder = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL); this.reverseOrder = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL);
Vue.set(this.room, "displayType", this.roomDisplayType); this.room["displayType"] = this.roomDisplayType;
// Listen to events // Listen to events
this.$matrix.on("Room.timeline", this.onEvent); this.$matrix.on("Room.timeline", this.onEvent);
@ -1300,8 +1299,8 @@ export default {
setParentThread(event) { setParentThread(event) {
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId); const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
if (parentEvent) { if (parentEvent) {
Vue.set(parentEvent, "isMxThread", true); parentEvent["isMxThread"] = true;
Vue.set(event, "parentThread", parentEvent); event["parentThread"] = parentEvent;
} else { } else {
// Try to load from server. // Try to load from server.
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId) this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId)
@ -1311,8 +1310,8 @@ export default {
if (parentEvent) { if (parentEvent) {
this.setEvents(this.timelineWindow.getEvents()); this.setEvents(this.timelineWindow.getEvents());
const fn = () => { const fn = () => {
Vue.set(parentEvent, "isMxThread", true); parentEvent["isMxThread"] = true;
Vue.set(event, "parentThread", parentEvent); event["parentThread"] = parentEvent;
}; };
if (this.initialLoadDone) { if (this.initialLoadDone) {
const sel = "[eventId=\"" + parentEvent.getId() + "\"]"; const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
@ -1334,7 +1333,7 @@ export default {
setReplyToEvent(event) { setReplyToEvent(event) {
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId); const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
if (parentEvent) { if (parentEvent) {
Vue.set(event, "replyEvent", parentEvent); event["replyEvent"] = parentEvent;
} else { } else {
// Try to load from server. // Try to load from server.
this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId) this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId)
@ -1343,7 +1342,7 @@ export default {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId); const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
if (parentEvent) { if (parentEvent) {
this.setEvents(this.timelineWindow.getEvents()); this.setEvents(this.timelineWindow.getEvents());
const fn = () => {Vue.set(event, "replyEvent", parentEvent);}; const fn = () => {event["replyEvent"] = parentEvent;};
if (this.initialLoadDone) { if (this.initialLoadDone) {
const sel = "[eventId=\"" + parentEvent.getId() + "\"]"; const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
const element = document.querySelector(sel); const element = document.querySelector(sel);
@ -1486,20 +1485,17 @@ export default {
outputType: "blob", outputType: "blob",
}) })
.then((img) => { .then((img) => {
Vue.set( fileObj["scaled"] =
fileObj,
"scaled",
new File([img], file.name, { new File([img], file.name, {
type: img.type, type: img.type,
lastModified: Date.now(), lastModified: Date.now(),
}) });
); fileObj["useScaled"] = true;
Vue.set(fileObj, "useScaled", true); fileObj["scaledSize"] = img.size;
Vue.set(fileObj, "scaledSize", img.size); fileObj["scaledDimensions"] = {
Vue.set(fileObj, "scaledDimensions", {
width: newWidth, width: newWidth,
height: newHeight, height: newHeight,
}); };
}) })
.catch((err) => { .catch((err) => {
console.error("Resize failed:", err); console.error("Resize failed:", err);

View file

@ -107,11 +107,11 @@
<v-col class="py-0"> <v-col class="py-0">
<v-checkbox id="chk-accept-ua" class="mt-0" v-model="acceptUA"> <v-checkbox id="chk-accept-ua" class="mt-0" v-model="acceptUA">
<template slot="label"> <template slot="label">
<i18n path="join.accept_ua" tag="span"> <i18n-t keypath="join.accept_ua" tag="span">
<template v-slot:agreement> <template v-slot:agreement>
<a href="./ua.html" target="_blank" @click.stop>{{ $t("join.ua") }}</a> <a href="./ua.html" target="_blank" @click.stop>{{ $t("join.ua") }}</a>
</template> </template>
</i18n> </i18n-t>
</template> </template>
</v-checkbox> </v-checkbox>
</v-col> </v-col>

View file

@ -7,12 +7,10 @@
:value="device.deviceId" :value="device.deviceId"
> >
<template v-slot:default="{ active }"> <template v-slot:default="{ active }">
<v-list-item-content>
<v-list-item-title>{{ displayName(device) }}</v-list-item-title> <v-list-item-title>{{ displayName(device) }}</v-list-item-title>
<v-list-item-subtitle>{{ <v-list-item-subtitle>{{
verificationStatus(device) verificationStatus(device)
}}</v-list-item-subtitle> }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="active"> <v-list-item-action v-if="active">
<v-btn icon> <v-btn icon>
<v-icon <v-icon

View file

@ -43,12 +43,10 @@
member.name.substring(0, 1).toUpperCase() member.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ memberName(member) }}</v-list-item-title> <v-list-item-title>{{ memberName(member) }}</v-list-item-title>
<v-list-item-subtitle <v-list-item-subtitle
v-text="member.userId" v-text="member.userId"
></v-list-item-subtitle> ></v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-btn icon v-if="active"> <v-btn icon v-if="active">
<v-icon color="grey lighten-1">check</v-icon> <v-icon color="grey lighten-1">check</v-icon>

View file

@ -60,11 +60,11 @@
<v-checkbox id="chk-accept-ua" class="mt-0" v-model="acceptUA"> <v-checkbox id="chk-accept-ua" class="mt-0" v-model="acceptUA">
<template slot="label"> <template slot="label">
<i18n path="join.accept_ua" tag="span"> <i18n-t keypath="join.accept_ua" tag="span">
<template v-slot:agreement> <template v-slot:agreement>
<a href="./ua.html" target="_blank" @click.stop>{{ $t("join.ua") }}</a> <a href="./ua.html" target="_blank" @click.stop>{{ $t("join.ua") }}</a>
</template> </template>
</i18n> </i18n-t>
</template> </template>
</v-checkbox> </v-checkbox>

View file

@ -17,7 +17,7 @@
v-if="$matrix.currentUser.is_guest && lastRoom" v-if="$matrix.currentUser.is_guest && lastRoom"
class="dialog-text" class="dialog-text"
> >
<i18n path="leave.text_public_lastroom" tag="p"> <i18n-t keypath="leave.text_public_lastroom" tag="p">
<template v-slot:user> <template v-slot:user>
<span>{{ $matrix.currentUserDisplayName }}</span> <span>{{ $matrix.currentUserDisplayName }}</span>
</template> </template>
@ -26,7 +26,7 @@
$t("leave.create_account") $t("leave.create_account")
}}</a> }}</a>
</template> </template>
</i18n> </i18n-t>
</div> </div>
<div v-else class="dialog-text">{{ $t("leave.text_public") }}</div> <div v-else class="dialog-text">{{ $t("leave.text_public") }}</div>
</template> </template>

View file

@ -269,6 +269,7 @@ import { mapState } from 'vuex'
export default { export default {
name: "Profile", name: "Profile",
mixins: [profileInfoMixin], mixins: [profileInfoMixin],
inject: ['$matrix'],
components: { components: {
ActionRow, ActionRow,
SelectLanguageDialog, SelectLanguageDialog,

View file

@ -7,7 +7,7 @@
<v-row> <v-row>
<v-col :class="['username', { 'editable': editDisplayName }]" cols="pa-2" ref="username"> <v-col :class="['username', { 'editable': editDisplayName }]" cols="pa-2" ref="username">
<div v-if="$matrix.currentUser.is_guest"> <div v-if="$matrix.currentUser.is_guest">
<i18n path="profile_info_popup.identity_temporary" tag="span"> <i18n-t keypath="profile_info_popup.identity_temporary" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<input v-model="displayName" <input v-model="displayName"
@keyup.enter="$event => $event.target.blur()" @keyup.enter="$event => $event.target.blur()"
@ -16,10 +16,10 @@
editDisplayName = !editDisplayName; editDisplayName = !editDisplayName;
" @focus="editDisplayName = !editDisplayName" /> " @focus="editDisplayName = !editDisplayName" />
</template> </template>
</i18n> </i18n-t>
</div> </div>
<div v-else> <div v-else>
<i18n path="profile_info_popup.identity" tag="span"> <i18n-t keypath="profile_info_popup.identity" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<input <input
v-model="displayName" v-model="displayName"
@ -28,7 +28,7 @@
@focus="editDisplayName = !editDisplayName" @focus="editDisplayName = !editDisplayName"
/> />
</template> </template>
</i18n> </i18n-t>
</div> </div>
</v-col> </v-col>
<v-col cols="auto" class="pa-2"> <v-col cols="auto" class="pa-2">
@ -44,12 +44,12 @@
<div class="want_more"> <div class="want_more">
🙌 {{ $t("profile_info_popup.want_more") }} 🙌 {{ $t("profile_info_popup.want_more") }}
</div> </div>
<i18n path="profile_info_popup.powered_by" tag="div"> <i18n-t keypath="profile_info_popup.powered_by" tag="div">
<template v-slot:product>{{ product }}</template> <template v-slot:product>{{ product }}</template>
<template v-slot:productLink> <template v-slot:productLink>
<a :href="'//' + productLink">{{ productLink }}</a> <a :href="'//' + productLink">{{ productLink }}</a>
</template> </template>
</i18n> </i18n-t>
<div class="text-end" v-if="!$config.hide_add_room_on_home"> <div class="text-end" v-if="!$config.hide_add_room_on_home">
<v-btn id="btn-new-room" class="new_room" text @click="createRoom"> <v-btn id="btn-new-room" class="new_room" text @click="createRoom">
{{ $t("profile_info_popup.new_room") }} {{ $t("profile_info_popup.new_room") }}

View file

@ -50,18 +50,18 @@
v-if="$matrix.currentUser.is_guest" v-if="$matrix.currentUser.is_guest"
class="d-inline-block me-2 white--text" class="d-inline-block me-2 white--text"
> >
<i18n path="profile_info_popup.identity_temporary" tag="span"> <i18n-t keypath="profile_info_popup.identity_temporary" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<b>{{ displayName }}</b> <b>{{ displayName }}</b>
</template> </template>
</i18n> </i18n-t>
</div> </div>
<div v-else class="d-inline-block me-2 white--text"> <div v-else class="d-inline-block me-2 white--text">
<i18n path="profile_info_popup.identity" tag="span"> <i18n-t keypath="profile_info_popup.identity" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<b>{{ displayName }}</b> <b>{{ displayName }}</b>
</template> </template>
</i18n> </i18n-t>
</div> </div>
<v-avatar <v-avatar
class="avatar-32 d-inline-block" class="avatar-32 d-inline-block"

View file

@ -53,7 +53,6 @@
</template> </template>
<script> <script>
import Vue from "vue";
import MessageIncomingText from "./messages/MessageIncomingText.vue"; import MessageIncomingText from "./messages/MessageIncomingText.vue";
import MessageIncomingFile from "./messages/MessageIncomingFile.vue"; import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue"; import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
@ -261,14 +260,14 @@ export default {
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => { this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => {
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId); const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
if (parentEvent) { if (parentEvent) {
Vue.set(parentEvent, "isMxThread", true); parentEvent["isMxThread"] = true;
Vue.set(event, "parentThread", parentEvent); event["parentThread"] = parentEvent;
} }
}); });
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => { this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => {
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId); const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
if (parentEvent) { if (parentEvent) {
Vue.set(event, "replyEvent", parentEvent); event["replyEvent"] = parentEvent;
} }
}); });

View file

@ -112,9 +112,7 @@
<v-list-item-avatar> <v-list-item-avatar>
<v-icon color="black">{{ item.icon }}</v-icon> <v-icon color="black">{{ item.icon }}</v-icon>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.text"></v-list-item-title> <v-list-item-title v-text="item.text"></v-list-item-title>
</v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-btn icon v-if="active"> <v-btn icon v-if="active">
<v-icon color="grey lighten-1">check</v-icon> <v-icon color="grey lighten-1">check</v-icon>
@ -142,7 +140,7 @@
<v-card-title class="h2">{{ $t("room_info.message_retention") }}</v-card-title> <v-card-title class="h2">{{ $t("room_info.message_retention") }}</v-card-title>
<v-card-text v-if="canViewRetentionPolicy"> <v-card-text v-if="canViewRetentionPolicy">
<v-list v-if="canChangeRetentionPolicy"> <v-list v-if="canChangeRetentionPolicy">
<v-list-item link v-on:click="showMessageRetentionDialog = true" class="px-0"> <v-list-item link v-on:click="showMessageRetentionDialog = true" class="px-0 pb-0">
<v-list-item-avatar class="mr-0 pb-0 mb-0"> <v-list-item-avatar class="mr-0 pb-0 mb-0">
<v-img <v-img
contain contain
@ -151,9 +149,7 @@
src="@/assets/icons/timer.svg" src="@/assets/icons/timer.svg"
/> />
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content class="pb-0">
{{ messageRetentionDisplay }} {{ messageRetentionDisplay }}
</v-list-item-content>
<v-list-item-action class="pb-0 mb-0"> <v-list-item-action class="pb-0 mb-0">
<v-icon>arrow_drop_down</v-icon> <v-icon>arrow_drop_down</v-icon>

View file

@ -6,11 +6,9 @@
<v-list-item-avatar class="round" size="42" color="#d9d9d9"> <v-list-item-avatar class="round" size="42" color="#d9d9d9">
<v-icon size="11">$vuetify.icons.ic_new_room</v-icon> <v-icon size="11">$vuetify.icons.ic_new_room</v-icon>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="room-list-new-room">{{ <v-list-item-title class="room-list-new-room">{{
$t("menu.new_room") $t("menu.new_room")
}}</v-list-item-title> }}</v-list-item-title>
</v-list-item-content>
</v-list-item> </v-list-item>
<!-- invites --> <!-- invites -->
@ -22,10 +20,8 @@
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="room-list-name">{{ room.name }}</v-list-item-title> <v-list-item-title class="room-list-name">{{ room.name }}</v-list-item-title>
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle> <v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-btn id="btn-accept" class="filled-button" depressed color="black" @click.stop="acceptInvitation(room)">{{ <v-btn id="btn-accept" class="filled-button" depressed color="black" @click.stop="acceptInvitation(room)">{{
$t("menu.join") }}</v-btn> $t("menu.join") }}</v-btn>
@ -41,14 +37,12 @@
room.name.substring(0, 1).toUpperCase() room.name.substring(0, 1).toUpperCase()
}}</span> }}</span>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class="room-list-name">{{ room.name }} <v-list-item-title class="room-list-name">{{ room.name }}
<!-- <v-icon class="ml-2 mb-1" size="10" v-if="isPublic(room)">$vuetify.icons.ic_public</v-icon> --> <!-- <v-icon class="ml-2 mb-1" size="10" v-if="isPublic(room)">$vuetify.icons.ic_public</v-icon> -->
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle class="room-list-new-messages" v-if="notificationCount(room) > 0"> <v-list-item-subtitle class="room-list-new-messages" v-if="notificationCount(room) > 0">
{{ $t("room.room_list_new_messages", { count: notificationCount(room) }) }} {{ $t("room.room_list_new_messages", { count: notificationCount(room) }) }}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-icon size="16" v-if="room.roomId == $matrix.currentRoomId">$vuetify.icons.ic_circle_filled</v-icon> <v-icon size="16" v-if="room.roomId == $matrix.currentRoomId">$vuetify.icons.ic_circle_filled</v-icon>
<v-icon size="16" v-else>$vuetify.icons.ic_circle</v-icon> <v-icon size="16" v-else>$vuetify.icons.ic_circle</v-icon>
@ -60,7 +54,6 @@
<script> <script>
import util from "../plugins/utils"; import util from "../plugins/utils";
import Vue from "vue";
import AuthedImage from "./AuthedImage.vue"; import AuthedImage from "./AuthedImage.vue";
export default { export default {
@ -143,7 +136,7 @@ export default {
}, },
acceptInvitation(room) { acceptInvitation(room) {
Vue.set(this.roomsProcessing, room.roomId, true); this.roomsProcessing[room.roomId] = true;
this.$matrix.matrixClient this.$matrix.matrixClient
.joinRoom(room.roomId) .joinRoom(room.roomId)
.then((ignoredRoom) => { .then((ignoredRoom) => {
@ -165,19 +158,19 @@ export default {
console.error("Failed to accept invite: ", err); console.error("Failed to accept invite: ", err);
}) })
.finally(() => { .finally(() => {
Vue.delete(this.roomsProcessing, room.roomId); delete this.roomsProcessing[room.roomId];
}); });
}, },
rejectInvitation(room) { rejectInvitation(room) {
Vue.set(this.roomsProcessing, room.roomId, true); this.roomsProcessing[room.roomId] = true;
this.$matrix this.$matrix
.leaveRoom(room.roomId) .leaveRoom(room.roomId)
.catch((err) => { .catch((err) => {
console.error("Failed to reject invite: ", err); console.error("Failed to reject invite: ", err);
}) })
.finally(() => { .finally(() => {
Vue.delete(this.roomsProcessing, room.roomId); delete this.roomsProcessing[room.roomId];
}); });
}, },

View file

@ -6,10 +6,8 @@
<template v-slot:selection="{ item }">{{ item.title }}</template> <template v-slot:selection="{ item }">{{ item.title }}</template>
<template v-slot:item="{ item, attrs, on }"> <template v-slot:item="{ item, attrs, on }">
<v-list-item v-on="on" v-bind="attrs" #default="{}"> <v-list-item v-on="on" v-bind="attrs" #default="{}">
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title> <v-list-item-title>{{ item.title }}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle> <v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
</template> </template>
</v-select> </v-select>

View file

@ -8,18 +8,18 @@
:outlined="!dark" :outlined="!dark"
>{{ $t("profile_info_popup.you_are") }}&nbsp; >{{ $t("profile_info_popup.you_are") }}&nbsp;
<span v-if="$matrix.currentUser.is_guest"> <span v-if="$matrix.currentUser.is_guest">
<i18n path="profile_info_popup.identity_temporary" tag="span"> <i18n-t keypath="profile_info_popup.identity_temporary" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<b>{{ displayName }}</b> <b>{{ displayName }}</b>
</template> </template>
</i18n> </i18n-t>
</span> </span>
<span v-else> <span v-else>
<i18n path="profile_info_popup.identity" tag="span"> <i18n-t keypath="profile_info_popup.identity" tag="span">
<template v-slot:displayName> <template v-slot:displayName>
<b>{{ displayName }}</b> <b>{{ displayName }}</b>
</template> </template>
</i18n> </i18n-t>
</span> </span>
<v-avatar color="#e0e0e0" right @click.stop="viewProfile"> <v-avatar color="#e0e0e0" right @click.stop="viewProfile">
<AuthedImage v-if="userAvatar" :src="userAvatar" /> <AuthedImage v-if="userAvatar" :src="userAvatar" />

View file

@ -25,7 +25,7 @@
> >
<v-list> <v-list>
<v-subheader class="text-uppercase"> {{ $tc("message.seen_by") }}</v-subheader> <v-subheader class="text-uppercase"> {{ $tc("message.seen_by") }}</v-subheader>
<v-list-item v-for="(member, index) in seenBy" :key="index"> <v-list-item v-for="(member, index) in seenBy" :key="index" class="text-left">
<v-list-item-icon> <v-list-item-icon>
<v-avatar size="40" color="grey"> <v-avatar size="40" color="grey">
<AuthedImage v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" /> <AuthedImage v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" />
@ -34,10 +34,8 @@
}}</span> }}</span>
</v-avatar> </v-avatar>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content class="text-left">
<v-list-item-title>{{member.roomMember.name}}</v-list-item-title> <v-list-item-title>{{member.roomMember.name}}</v-list-item-title>
<v-list-item-subtitle>{{ seenByTimeStamp(member.readTimestamp) }}</v-list-item-subtitle> <v-list-item-subtitle>{{ seenByTimeStamp(member.readTimestamp) }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
</BottomSheet> </BottomSheet>

View file

@ -3,27 +3,19 @@
<v-list dense> <v-list dense>
<v-list-item key="edit" v-if="isEditable" @click.stop="edit"> <v-list-item key="edit" v-if="isEditable" @click.stop="edit">
<v-list-item-icon><v-icon>$vuetify.icons.ic_edit</v-icon></v-list-item-icon> <v-list-item-icon><v-icon>$vuetify.icons.ic_edit</v-icon></v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ $t("menu.edit") }}</v-list-item-title> <v-list-item-title>{{ $t("menu.edit") }}</v-list-item-title>
</v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item key="pin" v-if="userCanPin && !event.isPinned" @click.stop="pin"> <v-list-item key="pin" v-if="userCanPin && !event.isPinned" @click.stop="pin">
<v-list-item-icon><v-icon>$vuetify.icons.ic_pin</v-icon></v-list-item-icon> <v-list-item-icon><v-icon>$vuetify.icons.ic_pin</v-icon></v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ $t("menu.pin") }}</v-list-item-title> <v-list-item-title>{{ $t("menu.pin") }}</v-list-item-title>
</v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item key="unpin" v-if="userCanPin && event.isPinned" @click.stop="unpin"> <v-list-item key="unpin" v-if="userCanPin && event.isPinned" @click.stop="unpin">
<v-list-item-icon><v-icon>$vuetify.icons.ic_pin</v-icon></v-list-item-icon> <v-list-item-icon><v-icon>$vuetify.icons.ic_pin</v-icon></v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ $t("menu.unpin") }}</v-list-item-title> <v-list-item-title>{{ $t("menu.unpin") }}</v-list-item-title>
</v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item key="redact" v-if="isRedactable" @click.stop="redact"> <v-list-item key="redact" v-if="isRedactable" @click.stop="redact">
<v-list-item-icon><v-icon color="#222222">delete_outline</v-icon></v-list-item-icon> <v-list-item-icon><v-icon color="#222222">delete_outline</v-icon></v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ $t("menu.delete") }}</v-list-item-title> <v-list-item-title>{{ $t("menu.delete") }}</v-list-item-title>
</v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
</div> </div>

View file

@ -9,6 +9,7 @@ linkify.options.defaults.className = "link";
linkify.options.defaults.target = { url: "_blank" }; linkify.options.defaults.target = { url: "_blank" };
export default { export default {
inject: ['$matrix'],
components: { components: {
QuickReactions, QuickReactions,
}, },

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="created-room-welcome-header"> <div class="created-room-welcome-header">
<div class="mt-2" v-if="roomJoinRule == 'public'"> <div class="mt-2" v-if="roomJoinRule == 'public'">
<i18n path="room_welcome.join_channel" tag="span"> <i18n-t keypath="room_welcome.join_channel" tag="span">
<template v-slot:link> <template v-slot:link>
<div style="position:relative;display:inline-block"> <div style="position:relative;display:inline-block">
<a @click.stop="copyPublicLink" :href="publicRoomLink" class="text-break">{{ publicRoomLink }}</a> <a @click.stop="copyPublicLink" :href="publicRoomLink" class="text-break">{{ publicRoomLink }}</a>
@ -10,17 +10,17 @@
$t("room_info.link_copied") }}</v-btn> $t("room_info.link_copied") }}</v-btn>
</div> </div>
</template> </template>
</i18n> </i18n-t>
</div> </div>
<div class="mt-2" v-else-if="roomJoinRule == 'invite'"> <div class="mt-2" v-else-if="roomJoinRule == 'invite'">
{{ $t("room_welcome.join_invite") }} {{ $t("room_welcome.join_invite") }}
</div> </div>
<div class="mt-2" v-if="roomMessageRetention() > 0"> <div class="mt-2" v-if="roomMessageRetention() > 0">
<i18n path="room_welcome.info_retention" tag="span"> <i18n-t keypath="room_welcome.info_retention" tag="span">
<template v-slot:time> <template v-slot:time>
<b>{{ messageRetentionDisplay }}</b> <b>{{ messageRetentionDisplay }}</b>
</template> </template>
</i18n>&nbsp; </i18n-t>&nbsp;
<a href="#" text @click.prevent="showMessageRetentionDialog = true" style="white-space: pre-line;">{{ $t("room_welcome.change") }}</a> <a href="#" text @click.prevent="showMessageRetentionDialog = true" style="white-space: pre-line;">{{ $t("room_welcome.change") }}</a>
</div> </div>
<div class="text-end"> <div class="text-end">

View file

@ -7,11 +7,11 @@
</a> </a>
</div> </div>
<div class="mt-2" v-if="roomMessageRetention() > 0"> <div class="mt-2" v-if="roomMessageRetention() > 0">
<i18n path="room_welcome.info_retention_user" tag="span"> <i18n-t keypath="room_welcome.info_retention_user" tag="span">
<template v-slot:time> <template v-slot:time>
<b>{{ messageRetentionDisplay }}</b> <b>{{ messageRetentionDisplay }}</b>
</template> </template>
</i18n> </i18n-t>
</div> </div>
</div> </div>
</template> </template>

View file

@ -2,7 +2,7 @@
<div class="created-room-welcome-header"> <div class="created-room-welcome-header">
<div>{{ $t("room_welcome.info") }}</div> <div>{{ $t("room_welcome.info") }}</div>
<div class="mt-2" v-if="roomJoinRule == 'public'"> <div class="mt-2" v-if="roomJoinRule == 'public'">
<i18n path="room_welcome.join_public" tag="span"> <i18n-t keypath="room_welcome.join_public" tag="span">
<template v-slot:link> <template v-slot:link>
<div style="position:relative;display:inline-block"> <div style="position:relative;display:inline-block">
<a @click.stop="copyPublicLink" :href="publicRoomLink" class="text-break">{{ publicRoomLink }}</a> <a @click.stop="copyPublicLink" :href="publicRoomLink" class="text-break">{{ publicRoomLink }}</a>
@ -17,7 +17,7 @@
> >
</div> </div>
</template> </template>
</i18n> </i18n-t>
</div> </div>
<div class="mt-2" v-else-if="roomJoinRule == 'invite'"> <div class="mt-2" v-else-if="roomJoinRule == 'invite'">
{{ $t("room_welcome.join_invite") }} {{ $t("room_welcome.join_invite") }}

View file

@ -1,4 +1,3 @@
import Vue from 'vue'
import App from './App.vue' import App from './App.vue'
import store from './store' import store from './store'
import i18n from './plugins/lang'; import i18n from './plugins/lang';
@ -10,40 +9,44 @@ 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 VueResize from 'vue-resize'; import VueResize from 'vue3-resize';
import 'vue-resize/dist/vue-resize.css'; import 'vue3-resize/dist/vue3-resize.css';
import VueClipboard from 'vue-clipboard2' import VueClipboard from 'vue-clipboard2'
import VueSanitize from "vue-sanitize"; import Vue3Sanitize from "vue-3-sanitize";
import createVuetify from './plugins/vuetify'; import createVuetify from './plugins/vuetify';
import { Buffer } from 'buffer/' import { Buffer } from 'buffer/'
import { createApp, h } from 'vue';
globalThis.Buffer = Buffer; globalThis.Buffer = Buffer;
var defaultOptions = VueSanitize.defaults; var defaultOptions = Vue3Sanitize.defaults;
defaultOptions.disallowedTagsMode = "recursiveEscape"; defaultOptions.disallowedTagsMode = "recursiveEscape";
defaultOptions.allowedTags = []; defaultOptions.allowedTags = [];
Vue.use(VueSanitize, defaultOptions);
Vue.config.productionTip = false const app = createApp({
render: () => h(App)
});
app.use(Vue3Sanitize, defaultOptions);
Vue.use(VueResize); app.config.productionTip = false
Vue.use(matrix, { store: store, i18n: i18n });
app.use(VueResize);
const configLoadedPromise = new Promise((resolve, ignoredreject) => { const configLoadedPromise = new Promise((resolve, ignoredreject) => {
// eslint-disable-next-line // eslint-disable-next-line
Vue.use(config, globalThis.window.location.origin, (config) => { app.use(config, globalThis.window.location.origin, (config) => {
resolve(config); resolve(config);
}); // Use this before cleaninsights below, it depends on config! }); // Use this before cleaninsights below, it depends on config!
}); });
Vue.use(analytics); app.use(analytics);
Vue.use(VueClipboard); app.use(VueClipboard);
Vue.use(audioPlayer); app.use(audioPlayer);
const vuetify = createVuetify(config); const vuetify = createVuetify(config);
// Add bubble functionality to custom events. // Add bubble functionality to custom events.
// From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events // From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events
Vue.use((Vue) => { app.use((instance) => {
Vue.prototype.$bubble = function $bubble(eventName, ...args) { instance.$bubble = function $bubble(eventName, ...args) {
// Emit the event on all parent components // Emit the event on all parent components
let component = this; let component = this;
let arg = args.at(0); let arg = args.at(0);
@ -62,7 +65,7 @@ Vue.use((Vue) => {
}); });
// Register a global custom directive called `v-blur` that prevents focus // Register a global custom directive called `v-blur` that prevents focus
Vue.directive('blur', { app.directive('blur', {
inserted: function (el) { inserted: function (el) {
el.onfocus = (ev) => ev.target.blur() el.onfocus = (ev) => ev.target.blur()
} }
@ -75,7 +78,7 @@ Vue.directive('blur', {
* *
* Like this: v-linkTap:500="[tapped,longTapped]" * Like this: v-linkTap:500="[tapped,longTapped]"
*/ */
Vue.directive('longTap', { app.directive('longTap', {
bind: function (el, binding, ignoredvnode) { bind: function (el, binding, ignoredvnode) {
el.longTapTimeout = parseInt(binding.arg || "500"); el.longTapTimeout = parseInt(binding.arg || "500");
el.longTapCallbacks = binding.value; el.longTapCallbacks = binding.value;
@ -168,27 +171,35 @@ Vue.directive('longTap', {
}, },
}); });
Vue.use(navigation, router); app.use(store);
app.$store = store;
app.config.globalProperties.$store = store;
const vueInstance = new Vue({ app.use(router);
vuetify, router.app = app;
store,
i18n, app.use(navigation, router);
router, app.use(vuetify);
matrix, app.use(i18n);
config,
analytics, app.use(matrix, { store: store, i18n: i18n });
audioPlayer,
render: h => h(App), //app.use(matrix);
}); //app.use(config);
if (vueInstance.$config.accentColor) { // app.use(analytics);
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor; // app.use(audioPlayer);
// app.render = h => {
// console.error("RENDERRENDERRENDERRENDERRENDSNAENNRENRANERNARE");
// h(App);
// }
if (app.$config.accentColor) {
app.$vuetify.theme.themes.light.primary = app.$config.accentColor;
} }
vueInstance.$audioPlayer.$root = vueInstance; // Make sure a $root is available here app.$audioPlayer.$root = app; // Make sure a $root is available here
configLoadedPromise.then((config) => { configLoadedPromise.then((config) => {
if (config.accentColor) { if (config.accentColor) {
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor; app.$vuetify.theme.themes.light.primary = config.accentColor;
} }
vueInstance.$mount('#app'); app.mount('#app');
}); });

View file

@ -1,7 +1,4 @@
import Vue from 'vue' import { createI18n } from 'vue-i18n'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
var messages = {} var messages = {}
@ -13,9 +10,10 @@ Object.keys(modules).map(path => {
messages[locale] = modules[path]; messages[locale] = modules[path];
}); });
const vue18n = new VueI18n({ const vue18n = new createI18n({
locale: 'en', locale: 'en',
fallbackLocale: 'en', fallbackLocale: 'en',
globalInjection: true,
silentFallbackWarn: true, silentFallbackWarn: true,
messages: messages, messages: messages,
pluralizationRules: { pluralizationRules: {

View file

@ -1,5 +1,4 @@
import Vue from 'vue'; import {createVuetify } from "vuetify";
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>
@ -12,11 +11,8 @@ Object.keys(modules).map(path => {
icons[iconName] = { component: modules[path].default } icons[iconName] = { component: modules[path].default }
}); });
Vue.use(Vuetify);
export default function(ignoredconfig) { export default function(ignoredconfig) {
return new Vuetify({ return createVuetify({
icons: { icons: {
iconfont: 'md', iconfont: 'md',
values: icons, values: icons,

View file

@ -1,5 +1,3 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../components/Home.vue' import Home from '../components/Home.vue'
import Chat from '../components/Chat.vue' import Chat from '../components/Chat.vue'
import Join from '../components/Join.vue' import Join from '../components/Join.vue'
@ -12,8 +10,7 @@ import CreateChannel from '../components/CreateChannel.vue'
import CreateFileDrop from '../components/CreateFileDrop.vue' import CreateFileDrop from '../components/CreateFileDrop.vue'
import User from '../models/user' import User from '../models/user'
import util from '../plugins/utils' import util from '../plugins/utils'
import { createRouter, createWebHashHistory } from 'vue-router'
Vue.use(VueRouter)
const routes = [ const routes = [
{ {
@ -110,8 +107,9 @@ const routes = [
} }
] ]
const router = new VueRouter({ const router = createRouter({
routes history: createWebHashHistory(),
routes: routes
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {

View file

@ -2,8 +2,8 @@ import cleaninsights from './cleaninsights.service'
import matomo from './matomo.service' import matomo from './matomo.service'
export default { export default {
install(Vue) { install(app) {
const analyticsService = new Vue({ const analyticsService = ({
data() { data() {
return { return {
engines: [], engines: [],
@ -60,6 +60,8 @@ export default {
} }
} }
}); });
Vue.prototype.$analytics = analyticsService;
app.$analytics = analyticsService;
app.config.globalProperties.$analytics = analyticsService;
} }
} }

View file

@ -9,7 +9,7 @@ import utils from "../plugins/utils";
* an audio matrix event and a unique component id (for example the ._uid property). * an audio matrix event and a unique component id (for example the ._uid property).
*/ */
export default { export default {
install(Vue) { install(app) {
class SharedAudioPlayer { class SharedAudioPlayer {
constructor() { constructor() {
this.player = new Audio(); this.player = new Audio();
@ -34,12 +34,12 @@ export default {
// Listeners is just a Set of component "uid" entries for now. // Listeners is just a Set of component "uid" entries for now.
entry = { url: null, listeners: new Set() }; entry = { url: null, listeners: new Set() };
// Make these reactive, so AudioPlayer (and others) can listen to them // Make these reactive, so AudioPlayer (and others) can listen to them
Vue.set(entry, "loading", false); entry["loading"] = false;
Vue.set(entry, "loadPercent", 0); entry["loadPercent"] = 0;
Vue.set(entry, "duration", 0); entry["duration"] = 0;
Vue.set(entry, "currentTime", 0); entry["currentTime"] = 0;
Vue.set(entry, "playPercent", 0); entry["playPercent"] = 0;
Vue.set(entry, "playing", false); entry["playing"] = false;
this.infoMap.set(eventId, entry); this.infoMap.set(eventId, entry);
// Get duration information // Get duration information
@ -263,6 +263,8 @@ export default {
} }
} }
Vue.prototype.$audioPlayer = new SharedAudioPlayer(); const audioPlayer = new SharedAudioPlayer();
app.$audioPlayer = audioPlayer;
app.config.globalProperties.$audioPlayer = audioPlayer;
}, },
}; };

View file

@ -1,9 +1,9 @@
import * as defaultConfig from "@/assets/config.json"; import * as defaultConfig from "@/assets/config.json";
export default { export default {
install(Vue, defaultServerFromLocation, onloaded) { install(app, defaultServerFromLocation, onloaded) {
var config = Vue.observable(defaultConfig.default); var config = defaultConfig.default;
Vue.set(config, "loaded", false); 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 => {
console.error("Failed to get config:", err); console.error("Failed to get config:", err);
@ -14,25 +14,25 @@ export default {
config.promise = getRuntimeConfig().then((json) => { config.promise = getRuntimeConfig().then((json) => {
// Reactively use all the config values // Reactively use all the config values
for (const key of Object.keys(json)) { for (const key of Object.keys(json)) {
Vue.set(config, key, json[key]); config[key] = json[key];
} }
// If default server is not set, default to current server address // If default server is not set, default to current server address
if (!json.defaultBaseUrl) { if (!json.defaultBaseUrl) {
if (json.defaultServer) { if (json.defaultServer) {
// TODO - Only to migrate old values (defaultServer was renamed defaultBaseUrl), can be removed later... // TODO - Only to migrate old values (defaultServer was renamed defaultBaseUrl), can be removed later...
Vue.set(config, "defaultBaseUrl", defaultServerFromLocation); config["defaultBaseUrl"] = defaultServerFromLocation;
} else { } else {
Vue.set(config, "defaultBaseUrl", json.defaultServer); config["defaultBaseUrl"] = json.defaultServer;
} }
} }
if (json.useFullyQualifiedDMLinks == undefined) { if (json.useFullyQualifiedDMLinks == undefined) {
Vue.set(config, "useFullyQualifiedDMLinks", true); // Default to true config["useFullyQualifiedDMLinks"] = true; // Default to true
} }
if (json.disableMediaSharing == undefined) { if (json.disableMediaSharing == undefined) {
Vue.set(config, "disableMediaSharing", false); Vue.set(config, "disableMediaSharing", false);
} }
if (!json.maxSizeAutoDownloads) { if (!json.maxSizeAutoDownloads) {
Vue.set(config, "maxSizeAutoDownloads", 10 * 1024 * 1024); config["maxSizeAutoDownloads"] = 10 * 1024 * 1024;
} }
if (!json.roomTypes) { if (!json.roomTypes) {
let roomTypes = ["group_chat", "channel"]; let roomTypes = ["group_chat", "channel"];
@ -40,9 +40,9 @@ export default {
if (fileDropEnabled) { if (fileDropEnabled) {
roomTypes.push("file_drop"); roomTypes.push("file_drop");
} }
Vue.set(config, "roomTypes", roomTypes); config["roomTypes"] = roomTypes;
} }
Vue.set(config, "loaded", true); config["loaded"] = true;
document.title = config.appName || ""; document.title = config.appName || "";
@ -70,6 +70,7 @@ export default {
return undefined; return undefined;
} }
Vue.prototype.$config = config; app.$config = config;
app.config.globalProperties.$config = config;
} }
} }

View file

@ -1,10 +1,11 @@
import { reactive, createApp } from "vue";
// import olm from "@matrix-org/olm/olm_legacy";
// global.Olm = olm;
import * as sdk from "matrix-js-sdk"; import * as sdk from "matrix-js-sdk";
import { TimelineWindow, EventTimeline, EventStatus } from "matrix-js-sdk"; import { TimelineWindow, EventTimeline } 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 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"; import { LocalStorageCryptoStore } from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store";
const LocalStorageCryptoStore = LocalStorageCryptoStoreClass.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
@ -16,11 +17,13 @@ export const CHANNEL_POWER_LEVELS = {
}; };
export default { export default {
install(Vue, options) { install(app, options) {
if (!options || !options.store) { if (!options || !options.store) {
throw new Error("Please initialise plugin with a Vuex store."); throw new Error("Please initialise plugin with a Vuex store.");
} }
const store = options.store;
// Set User-Agent headers. // Set User-Agent headers.
// Update: browser do not allow this, "Refused to set unsafe header "User-Agent"" // Update: browser do not allow this, "Refused to set unsafe header "User-Agent""
// Keep this code around however, since it's an example of how to add headers to a request... // Keep this code around however, since it's an example of how to add headers to a request...
@ -30,12 +33,7 @@ export default {
// var ret = orig(opts, callback); // var ret = orig(opts, callback);
// return ret; // return ret;
// }); // });
const store = options.store; const matrixService = createApp({
const i18n = options.i18n;
const matrixService = new Vue({
store,
i18n,
data() { data() {
return { return {
matrixClient: null, matrixClient: null,
@ -44,13 +42,10 @@ export default {
userDisplayName: null, userDisplayName: null,
userAvatar: null, userAvatar: null,
currentRoom: null, currentRoom: null,
userCanSendMessageInCurrentRoom: true,
userCanSendReactionAndAnswerPollInCurrentRoom: true,
currentRoomBeingPurged: false,
notificationCount: 0, notificationCount: 0,
useAuthedMedia: false,
}; };
}, },
mounted() { mounted() {
console.log("Matrix service mounted"); console.log("Matrix service mounted");
}, },
@ -77,8 +72,8 @@ export default {
return null; return null;
}, },
currentUserMXDomain() { currentUserHomeServer() {
return User.domainPart(this.currentUserId) || this.$config.defaultMatrixDomainPart; return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId);
}, },
currentRoomId() { currentRoomId() {
@ -96,12 +91,6 @@ export default {
return room.selfMembership === "invite"; return room.selfMembership === "invite";
}); });
}, },
joinedAndInvitedRooms() {
return this.rooms.filter((room) => {
return room.selfMembership === "join" || room.selfMembership === "invite";
});
},
}, },
watch: { watch: {
@ -111,18 +100,6 @@ export default {
this.currentRoom = this.getRoom(roomId); this.currentRoom = this.getRoom(roomId);
}, },
}, },
currentRoom: {
immediate: true,
handler(room) {
if (room) {
this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(room.roomId, this.currentUserId);
this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(room.roomId, this.currentUserId);
} else {
this.userCanSendMessageInCurrentRoom = true;
this.userCanSendReactionAndAnswerPollInCurrentRoom = true;
}
},
},
}, },
methods: { methods: {
@ -130,11 +107,9 @@ export default {
console.log("create crypto store"); console.log("create crypto store");
return new LocalStorageCryptoStore(this.$store.getters.storage); return new LocalStorageCryptoStore(this.$store.getters.storage);
}, },
login(user, registrationFlowHandler, createUser = false) { login(user) {
return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
const tempMatrixClient = sdk.createClient({ const tempMatrixClient = sdk.createClient({
baseUrl: baseUrl, baseUrl: user.home_server,
idBaseUrl: this.$config.identityServer,
}); });
var promiseLogin; var promiseLogin;
@ -142,52 +117,27 @@ export default {
if (user.access_token) { if (user.access_token) {
// Logged in on "real" account // Logged in on "real" account
promiseLogin = Promise.resolve(user); promiseLogin = Promise.resolve(user);
} else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) { } else if (user.is_guest && !user.user_id) {
// Generate random username and password. We don't user REAL matrix // Generate random username and password. We don't user REAL matrix
// guest accounts because 1. They are not allowed to post media, 2. They // guest accounts because 1. They are not allowed to post media, 2. They
// can not use avatars and 3. They can not seamlessly be upgraded to real accounts. // can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
// //
// Instead, we use an ILAG approach, Improved Landing as Guest. // Instead, we use an ILAG approach, Improved Landing as Guest.
const userId = const user = util.randomUser(this.$config.userIdPrefix);
createUser || user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix); const pass = util.randomPass();
const pass = createUser || user.registration_session ? user.password : util.randomPass();
const extractAndSaveUser = (response) => {
var u = Object.assign({}, response);
u.password = pass;
u.is_guest = true;
this.$store.commit("setUser", u);
return u;
};
promiseLogin = tempMatrixClient promiseLogin = tempMatrixClient
.register(userId, pass, user.registration_session || null, { .register(user, pass, null, {
type: "m.login.dummy", type: "m.login.dummy",
initial_device_display_name: this.$config.appName, initial_device_display_name: this.$config.appName,
}) })
.then((response) => { .then((response) => {
return extractAndSaveUser(response); console.log("Response", response);
}) var u = Object.assign({}, response);
.catch((error) => { u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
if (registrationFlowHandler && error.httpStatus == 401 && error.data) {
const registrationSession = error.data.session;
// Store user, pass and session, so we can resume if network failure occurs etc.
//
var u = {};
u.user_id = userId;
u.password = pass; u.password = pass;
u.is_guest = true; u.is_guest = true;
u.registration_session = registrationSession;
this.$store.commit("setUser", u); this.$store.commit("setUser", u);
return u;
return registrationFlowHandler(tempMatrixClient, error.data).then((response) =>
extractAndSaveUser(response)
);
} else {
console.error(error);
}
throw error;
}); });
} else { } else {
var data = { var data = {
@ -205,6 +155,7 @@ export default {
// Copy over needed properties // Copy over needed properties
u = Object.assign(user, response); u = Object.assign(user, response);
} }
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
this.$store.commit("setUser", u); this.$store.commit("setUser", u);
return u; return u;
}); });
@ -213,7 +164,6 @@ export default {
return promiseLogin.then((user) => { return promiseLogin.then((user) => {
return self.getMatrixClient(user); return self.getMatrixClient(user);
}); });
});
}, },
clearCryptoStore() { clearCryptoStore() {
@ -287,25 +237,22 @@ export default {
const matrixStore = new sdk.MemoryStore(this.$store.getters.storage); const matrixStore = new sdk.MemoryStore(this.$store.getters.storage);
return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
var opts = { var opts = {
baseUrl: baseUrl, baseUrl: user.home_server,
userId: user.user_id, userId: user.user_id,
store: matrixStore, store: matrixStore,
deviceId: user.device_id, deviceId: user.device_id,
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
.initRustCrypto() .initCrypto()
.then(() => { .then(() => {
console.log("Crypto initialized"); console.log("Crypto initialized");
@ -332,16 +279,10 @@ 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;
}); });
});
}, },
/** /**
@ -350,14 +291,11 @@ export default {
* Will use a real account, if we have one, otherwise will create * Will use a real account, if we have one, otherwise will create
* a random account. * a random account.
*/ */
getLoginPromise(registrationFlowHandler) { getLoginPromise() {
if (this.ready) { if (this.ready) {
return Promise.resolve(this.currentUser); return Promise.resolve(this.currentUser);
} }
return this.$store.dispatch("login", { return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true));
user: this.currentUser || new User("", "", true),
registrationFlowHandler,
});
}, },
addMatrixClientListeners(client) { addMatrixClientListeners(client) {
@ -385,7 +323,7 @@ export default {
{ {
const room = this.matrixClient.getRoom(event.getRoomId()); const room = this.matrixClient.getRoom(event.getRoomId());
if (room) { if (room) {
Vue.set(room, "topic", event.getContent().topic); room.topic = event.getContent().topic;
} }
} }
break; break;
@ -394,56 +332,24 @@ export default {
{ {
const room = this.matrixClient.getRoom(event.getRoomId()); const room = this.matrixClient.getRoom(event.getRoomId());
if (room) { if (room) {
Vue.set( room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true);
room,
"avatar",
room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia)
);
} }
} }
break; break;
case "m.room.power_levels": case "m.room.member":
{ {
if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(event.getRoomId(), this.currentUserId); // Don't use this.currentRoomId, may be an alias. We need the real id!
this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(event.getRoomId(), this.currentUserId); if (
} event.getContent().membership == "leave" &&
} (event.getPrevContent() || {}).membership == "join" &&
break; event.getStateKey() == this.currentUserId &&
event.getSender() != this.currentUserId
case "m.room.join_rules": ) {
{ // We were kicked
const room = this.matrixClient.getRoom(event.getRoomId()); const wasPurged = event.getContent().reason == "Room Deleted";
if (room && room.getJoinRule() == "private" && room.selfMembership == "invite") { this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
// We have an invite to a room that's now "private"? This is most probably a deleted DM room.
// Reject the invite, i.e. call "leave" on it.
this.matrixClient.leave(room.roomId);
}
}
break;
case "m.room.canonical_alias":
{
if (this.currentRoomId && this.currentRoomId.startsWith("#") && !this.currentRoom) {
this.currentRoom = this.getRoom(this.currentRoomId);
}
}
break;
case STATE_EVENT_ROOM_DELETED:
{
const room = this.matrixClient.getRoom(event.getRoomId());
if (room && room.currentState) {
// Before we do anything, make sure the sender is an admin!
// Also, do not react if WE are the sender, since we are probably
// busy doing the rest of the purging process...
if (room.currentState.maySendStateEvent("m.room.power_levels", event.getSender())) {
if (event.getSender() !== this.currentUserId) {
this.leaveRoomAndNavigate(room.roomId).then(() => {
this.matrixClient.forget(room.roomId, true);
});
}
} }
} }
} }
@ -452,19 +358,8 @@ export default {
this.updateNotificationCount(); this.updateNotificationCount();
}, },
onRoom(room) { onRoom(ignoredroom) {
if (room.selfMembership === "invite") { console.log("Got room", ignoredroom);
this.matrixClient
.getRoomTags(room.roomId)
.then((reply) => {
if (Object.keys(reply.tags).includes("m.server_notice")) {
Vue.set(room, "isServiceNoticeRoom", true);
}
})
.catch((error) => {
console.error(error);
});
}
this.reloadRooms(); this.reloadRooms();
this.updateNotificationCount(); this.updateNotificationCount();
}, },
@ -519,25 +414,19 @@ export default {
// each time! // each time!
var updatedRooms = this.matrixClient.getVisibleRooms(); var updatedRooms = this.matrixClient.getVisibleRooms();
updatedRooms = updatedRooms.filter((room) => { updatedRooms = updatedRooms.filter((room) => {
return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join") && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0; return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join");
}); });
updatedRooms.forEach((room) => { updatedRooms.forEach((room) => {
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia)); if (!room.avatar) {
room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true);
}
}); });
Vue.set(this, "rooms", updatedRooms); console.log("Reload rooms", updatedRooms);
this.rooms = updatedRooms;
const resolvedId = const currentRoom = this.getRoom(this.$store.state.currentRoomId);
this.currentRoomId && this.currentRoomId.startsWith("#")
? this.matrixClient.getRoomIdForAlias(this.currentRoomId).then((r) => r.room_id)
: Promise.resolve(this.currentRoomId);
resolvedId
.then((roomId) => {
const currentRoom = this.getRoom(roomId);
if (this.currentRoom != currentRoom) { if (this.currentRoom != currentRoom) {
this.currentRoom = currentRoom; this.currentRoom = currentRoom;
} }
})
.catch((ignorederror) => {});
}, },
setCurrentRoomId(roomId) { setCurrentRoomId(roomId) {
@ -603,178 +492,15 @@ export default {
}, },
leaveRoom(roomId) { leaveRoom(roomId) {
return this.matrixClient.leave(roomId).then(() => { return this.matrixClient.leave(roomId, undefined).then(() => {
this.$store.commit("setCurrentRoomId", null);
this.rooms = this.rooms.filter((room) => { this.rooms = this.rooms.filter((room) => {
room.roomId != roomId; room.roomId != roomId;
}); });
//this.matrixClient.store.removeRoom(roomId); this.matrixClient.store.removeRoom(roomId);
//this.matrixClient.forget(roomId, true, undefined); //this.matrixClient.forget(roomId, true, undefined);
}); });
}, },
/**
* Leave the room, and if this is the last room we are in, navigate to the "goodbye" page.
* Otherwise, navigate to home.
* @param roomId
*/
leaveRoomAndNavigate(roomId) {
const joinedRooms = this.joinedRooms;
const isLastRoomWeAreJoinedTo = joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == roomId;
return this.leaveRoom(roomId).then(() => {
if (isLastRoomWeAreJoinedTo) {
this.$navigation.push({ name: "Goodbye" }, -1);
} else {
this.$navigation.push({ name: "Home", params: { roomId: null } }, -1);
}
});
},
kickUser(roomId, userId) {
if (this.matrixClient && roomId && userId) {
this.matrixClient.kick(roomId, userId, "");
}
},
banUser(roomId, userId) {
if (this.matrixClient && roomId && userId) {
this.matrixClient.ban(roomId, userId, "");
}
},
makeAdmin(roomId, userId) {
if (this.matrixClient && roomId && userId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (powerLevelEvent) {
this.matrixClient.setPowerLevel(roomId, userId, 100, powerLevelEvent);
}
}
}
},
makeModerator(roomId, userId) {
if (this.matrixClient && roomId && userId) {
const room = this.getRoom(roomId);
console.log("Room", room);
if (room && room.currentState) {
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (powerLevelEvent) {
this.matrixClient.setPowerLevel(roomId, userId, 50, powerLevelEvent);
}
}
}
},
revokeModerator(roomId, userId) {
if (this.matrixClient && roomId && userId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (powerLevelEvent) {
this.matrixClient.setPowerLevel(roomId, userId, 0, powerLevelEvent);
}
}
}
},
/**
* Returns true if the current user is joined to the given room.
* @param roomIdOrAlias
* @returns Promise<Bool> - Whether the user is joined to the room or not
*/
isJoinedToRoom(roomIdOrAlias) {
if (roomIdOrAlias && this.matrixClient) {
try {
const resolvedRoomId = roomIdOrAlias.startsWith("#")
? this.matrixClient.getRoomIdForAlias(roomIdOrAlias).then((res) => res.room_id)
: Promise.resolve(roomIdOrAlias);
return resolvedRoomId.then((roomId) => {
return this.matrixClient.getJoinedRooms().then((rooms) => {
return rooms.joined_rooms.includes(roomId);
});
});
} catch (ignorederror) {
console.error(ignorederror);
return Promise.resolve(false);
}
}
return Promise.resolve(false);
},
isReadOnlyRoom(roomId) {
if (this.matrixClient && roomId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (powerLevelEvent) {
if (this.roomType(roomId) == ROOM_TYPE_CHANNEL) {
return Object.keys(powerLevelEvent.getContent().events).length == 0;
}
return powerLevelEvent.getContent().events_default > 0;
}
}
}
return false;
},
setReadOnlyRoom(roomId, readOnly) {
if (this.matrixClient && roomId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (powerLevelEvent) {
let content = powerLevelEvent.getContent();
if (this.roomType(roomId) == ROOM_TYPE_CHANNEL) {
content.events = readOnly ? {} : CHANNEL_POWER_LEVELS
} else {
content.events_default = readOnly ? 50 : 0;
}
this.matrixClient.sendStateEvent(room.roomId, "m.room.power_levels", content);
}
}
}
},
userCanSendMessageInRoom(roomId, userId) {
if (this.matrixClient && roomId && userId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
let isAdmin = room.currentState.maySendEvent("m.room.power_levels", this.currentUserId);
return isAdmin || (this.roomType(roomId) != ROOM_TYPE_CHANNEL && !this.isReadOnlyRoom(roomId));
}
}
return true;
},
userCanSendReactionAndAnswerPollInRoom(roomId, userId) {
if (this.matrixClient && roomId && userId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
let isAdmin = room.currentState.maySendEvent("m.room.power_levels", this.currentUserId);
return isAdmin || !this.isReadOnlyRoom(roomId);
}
}
return true;
},
roomType(roomId) {
if (this.matrixClient && roomId) {
const room = this.getRoom(roomId);
if (room && room.currentState) {
const roomTypeEvent = room.currentState.getStateEvents(STATE_EVENT_ROOM_TYPE, "") || room.currentState.getStateEvents("m.room.create", "");
if (roomTypeEvent) {
const roomType = roomTypeEvent.getContent().type;
if ([ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE, ROOM_TYPE_CHANNEL].includes(roomType)) {
return roomType;
}
}
}
}
return ROOM_TYPE_DEFAULT;
},
/** /**
* Purge the room with the given id! This means: * Purge the room with the given id! This means:
* - Make room invite only * - Make room invite only
@ -785,30 +511,6 @@ export default {
* @param roomId * @param roomId
*/ */
purgeRoom(roomId, statusCallback) { purgeRoom(roomId, statusCallback) {
this.currentRoomBeingPurged = true;
//console.log("Purge room");
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const withRetry = (codeBlock) => {
return codeBlock().catch((error) => {
if (error && error.errcode == "M_LIMIT_EXCEEDED") {
var retry = 1000;
if (error.data) {
const retryIn = error.data.retry_after_ms;
retry = Math.max(retry, retryIn ? retryIn : 0);
}
console.log("Rate limited, retry in", retry);
return sleep(retry).then(() => withRetry(codeBlock));
} else {
return Promise.resolve();
}
});
};
const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices(); const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const room = this.getRoom(roomId); const room = this.getRoom(roomId);
@ -817,39 +519,22 @@ export default {
return; return;
} }
// Remove any possible pending events
room.getLiveTimeline().getEvents().filter((e) => [EventStatus.ENCRYPTING, sdk.EventStatus.QUEUED].includes(e.status)).forEach((e) => {
//console.log("Cancel pending event!");
this.matrixClient.cancelPendingEvent(e);
});
const timelineWindow = new TimelineWindow(this.matrixClient, room.getUnfilteredTimelineSet(), {}); const timelineWindow = new TimelineWindow(this.matrixClient, room.getUnfilteredTimelineSet(), {});
const self = this; const self = this;
//console.log("Purge: set invite only"); //console.log("Purge: set invite only");
statusCallback(this.$t("room.purge_set_room_state")); statusCallback(this.$t("room.purge_set_room_state"));
withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "private" }, "")) this.matrixClient
.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "invite" }, "")
.then(() => { .then(() => {
//console.log("Purge: forbid guest access"); //console.log("Purge: forbid guest access");
return withRetry(() => this.matrixClient.sendStateEvent( return this.matrixClient.sendStateEvent(roomId, "m.room.guest_access", { guest_access: "forbidden" }, "");
roomId,
"m.room.guest_access",
{ guest_access: "forbidden" },
""
));
}) })
.then(() => { .then(() => {
//console.log("Purge: set history visibility to 'joined'"); //console.log("Purge: set history visibility to 'joined'");
return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", { return this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", {
history_visibility: "joined", history_visibility: "joined",
})); });
})
.then(() => {
return withRetry(() => this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" }));
})
.then(() => {
// Set message retention 1 minute (may be limited by server)
return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.retention", { max_lifetime: 60000 }));
}) })
.then(() => { .then(() => {
//console.log("Purge: create timeline"); //console.log("Purge: create timeline");
@ -859,11 +544,8 @@ export default {
const getMoreIfAvailable = function _getMoreIfAvailable() { const getMoreIfAvailable = function _getMoreIfAvailable() {
if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) { if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) {
//console.log("Purge: page back"); //console.log("Purge: page back");
return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((gotmore) => { return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((ignoredsuccess) => {
if (gotmore) {
return _getMoreIfAvailable.call(self); return _getMoreIfAvailable.call(self);
}
return Promise.resolve("Done");
}); });
} else { } else {
return Promise.resolve("Done"); return Promise.resolve("Done");
@ -876,92 +558,40 @@ export default {
statusCallback(this.$t("room.purge_redacting_events")); statusCallback(this.$t("room.purge_redacting_events"));
// First ignore unknown device errors // First ignore unknown device errors
this.matrixClient.setGlobalErrorOnUnknownDevices(false); this.matrixClient.setGlobalErrorOnUnknownDevices(false);
const allEvents = timelineWindow.getEvents().filter((event) => { var redactionPromises = [];
return ( timelineWindow.getEvents().forEach((event) => {
!event.isRedacted() && if (!event.isRedacted() && !event.isRedaction() && !event.isState()) {
!event.isRedaction() && // Redact!
!event.isState() && redactionPromises.push(this.matrixClient.redactEvent(event.getRoomId(), event.getId()));
(!room.currentState || room.currentState.maySendRedactionForEvent(event, this.currentUserId))
);
});
const redactFirstEvent = (events) => {
statusCallback(
this.$t("room.purge_redacting_events", {
count: allEvents.length - events.length + 1,
total: allEvents.length,
})
);
if (events.length == 0) {
return Promise.resolve(true);
} }
const event = events[0]; });
return withRetry(() => this.matrixClient.redactEvent(event.getRoomId(), event.getId())).then(() => return Promise.all(redactionPromises);
redactFirstEvent(events.slice(1))
);
};
return redactFirstEvent(allEvents);
}) })
.then(() => { .then(() => {
//console.log("Purge: kick members"); //console.log("Purge: kick members");
statusCallback(this.$t("room.purge_removing_members")); statusCallback(this.$t("room.purge_removing_members"));
var joined = room.getMembersWithMembership("join"); var joined = room.getMembersWithMembership("join");
var invited = room.getMembersWithMembership("invite"); var invited = room.getMembersWithMembership("invite");
var allMembers = joined.concat(invited); var members = joined.concat(invited);
const me = allMembers.find((m) => m.userId == self.currentUserId); var kickPromises = [];
members.forEach((member) => {
const kickFirstMember = (members) => { if (member.userId != self.currentUserId) {
//console.log(`Kicking ${members.length} members`); kickPromises.push(this.matrixClient.kick(roomId, member.userId, "Room Deleted"));
statusCallback(
this.$t("room.purge_removing_members", {
count: allMembers.length - members.length + 1,
total: allMembers.length,
})
);
if (members.length == 0) {
return Promise.resolve(true);
} }
const member = members[0]; });
if (member.userId == self.currentUserId) { return Promise.all(kickPromises);
return kickFirstMember(members.slice(1));
} else {
// Slight pause to avoid rate limiting.
return sleep(0.1)
.then(() => withRetry(() => {
if (member.membership == "invite" && me && me.powerLevel <= member.powerLevel) {
// The user is invited, but we can't kick them because of power levels.
// Send a new invite with reason set to "Room Deleted".
// The client will be sent stripped room state, and can from that see the
// join_rule of "private". It will then "leave", i.e. reject the invite.
return this.matrixClient.invite(roomId, member.userId, "Room Deleted");
}
return this.matrixClient.kick(roomId, member.userId, "Room Deleted")
}))
.then(() => kickFirstMember(members.slice(1)));
}
};
return kickFirstMember(allMembers);
})
.then(() => {
return withRetry(() =>
this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" })
);
}) })
.then(() => { .then(() => {
statusCallback(null); statusCallback(null);
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
return withRetry(() => this.leaveRoom(roomId)); return this.leaveRoom(roomId);
}) })
.then(() => { .then(() => {
this.currentRoomBeingPurged = false;
resolve(true); // Done! resolve(true); // Done!
}) })
.catch((err) => { .catch((err) => {
console.error("Error purging room", err); console.error("Error purging room", err);
this.currentRoomBeingPurged = false;
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
reject(err); reject(err);
}); });
@ -1017,17 +647,7 @@ export default {
type: "m.room.history_visibility", type: "m.room.history_visibility",
state_key: "", state_key: "",
content: { content: {
history_visibility: "invited", history_visibility: "joined",
},
},
{
type: "m.room.power_levels",
state_key: "",
content: {
users: {
[this.currentUserId]: 100,
[userId]: 100,
},
}, },
}, },
], ],
@ -1050,7 +670,7 @@ export default {
* @param {*} userId * @param {*} userId
*/ */
isDirectRoomWith(room, userId) { isDirectRoomWith(room, userId) {
if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) { if (room.getJoinRule() == "invite" && room.getMembers().length == 2) {
let other = room.getMember(userId); let other = room.getMember(userId);
if (other) { if (other) {
if (room.getMyMembership() == "invite" && other.membership == "join") { if (room.getMyMembership() == "invite" && other.membership == "join") {
@ -1065,20 +685,6 @@ export default {
return false; return false;
}, },
/**
* Return true if this room is a direct room with one other user. NOTE: this currently
* only checks number of members, not any is_direct flag.
* @param { } room
*/
isDirectRoom(room) {
// TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
// apprently need to set this...
if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) {
return true;
}
return false;
},
on(event, handler) { on(event, handler) {
if (this.matrixClient) { if (this.matrixClient) {
this.matrixClient.on(event, handler); this.matrixClient.on(event, handler);
@ -1091,17 +697,6 @@ export default {
} }
}, },
setUserDisplayName(name) {
if (this.matrixClient) {
return this.matrixClient
.setDisplayName(name || this.user.userId)
.then(() => (this.userDisplayName = name))
.catch((err) => console.err("Failed to set display name", err));
} else {
return Promise.reject("No matrix client");
}
},
setPassword(oldPassword, newPassword) { setPassword(oldPassword, newPassword) {
if (this.matrixClient && this.currentUser) { if (this.matrixClient && this.currentUser) {
const authDict = { const authDict = {
@ -1135,26 +730,31 @@ export default {
return this.matrixClient.uploadContent(file, opts); return this.matrixClient.uploadContent(file, opts);
}, },
/** getPublicRoomInfo(roomId) {
* Get a matrix client that can be used for public queries. If we are logged in, this is the normal if (!roomId) {
* matrix client. If not, we create a temp one with a temp password. return Promise.reject("Invalid parameters");
* @returns A MatrixClient that can be used for public queries }
*/
getPublicQueryMatrixClient() { const parts = roomId.split(":");
if (parts.length != 2) {
return Promise.reject("Unknown room server");
}
const server = parts[1];
var clientPromise;
if (this.matrixClient) { if (this.matrixClient) {
return this.getMatrixClient().then(() => { clientPromise = this.getMatrixClient().then(() => {
return this.matrixClient; return this.matrixClient;
}); });
} else { } else {
const tempMatrixClient = sdk.createClient({
baseUrl: this.$config.defaultServer,
});
var tempUserString = this.$store.state.tempuser; var tempUserString = this.$store.state.tempuser;
var tempUser = null; var tempUser = null;
if (tempUserString) { if (tempUserString) {
tempUser = JSON.parse(tempUserString); tempUser = JSON.parse(tempUserString);
} }
return util.getMatrixBaseUrl(tempUser, this.$config).then((baseUrl) => {
const tempMatrixClient = sdk.createClient({ baseUrl: baseUrl });
var clientPromise;
// Need to create an account? // Need to create an account?
// //
@ -1197,7 +797,7 @@ export default {
// Only used to get public room info from. // Only used to get public room info from.
clientPromise = clientPromise.then((user) => { clientPromise = clientPromise.then((user) => {
var opts = { var opts = {
baseUrl: baseUrl, baseUrl: this.$config.defaultServer,
userId: user.user_id, userId: user.user_id,
accessToken: user.access_token, accessToken: user.access_token,
timelineSupport: false, timelineSupport: false,
@ -1206,33 +806,16 @@ export default {
matrixClient.startClient(); matrixClient.startClient();
return matrixClient; return matrixClient;
}); });
return clientPromise;
});
}
},
getPublicRoomInfo(roomId) {
if (!roomId) {
return Promise.reject("Invalid parameters");
} }
const parts = roomId.split(":"); const findOrGetMore = function _findOrGetMore(client, response) {
if (parts.length != 2) {
return Promise.reject("Unknown room server");
}
const server = parts[1];
const clientPromise = this.getPublicQueryMatrixClient();
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) {
Vue.set(room, "avatar", client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", false, undefined, useAuthedMedia)); room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", true);
} }
return Promise.resolve(room); return Promise.resolve(room);
} }
@ -1256,18 +839,13 @@ 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, useAuthedMedia, response); return findOrGetMore(matrixClient, response);
}) })
.catch((err) => { .catch((err) => {
return Promise.reject("Failed to find room: " + err); return Promise.reject("Failed to find room: " + err);
@ -1281,38 +859,20 @@ export default {
}); });
this.notificationCount = count; this.notificationCount = count;
}, },
setEventPinned(room, event, pinned) {
if (room && room.currentState && event) {
const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {}
let pinnedEvents = content["pinned"] || [];
if (pinned && !pinnedEvents.includes(event.getId())) {
pinnedEvents.push(event.getId());
} else if (!pinned && pinnedEvents.includes(event.getId())) {
pinnedEvents = pinnedEvents.filter((e) => e != event.getId());
} else {
return; // no change
}
content.pinned = pinnedEvents;
this.matrixClient.sendStateEvent(room.roomId, "m.room.pinned_events", content);
}
},
getPinnedEvents(room) {
if (room && room.currentState) {
const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {}
return content["pinned"] || [];
} else {
return [];
}
},
}, },
render: () => {}
}); });
matrixService.config.globalProperties.$store = store;
sdk.setCryptoStoreFactory(matrixService.createCryptoStore.bind(matrixService)); matrixService.config.globalProperties.$config = app.$config;
matrixService.config.globalProperties.$t = app.$t;
Vue.prototype.$matrix = matrixService; matrixService.config.globalProperties.$navigation = app.$navigation;
matrixService.$store = store;
matrixService.$config = app.$config;
matrixService.$t = app.$t;
matrixService.$navigation = app.$navigation;
const instance = matrixService.mount("#app2");
app.config.globalProperties.$matrix = instance;
app.$matrix = instance;
sdk.setCryptoStoreFactory(instance.createCryptoStore.bind(instance));
}, },
}; };

1281
src/services/matrix.vue Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
export default { export default {
install(Vue, router) { install(app, router) {
var routes = []; var routes = [];
var nextRoutes = null; var nextRoutes = null;
var zeroIndex = undefined; var zeroIndex = undefined;
@ -75,6 +75,8 @@ export default {
router.go(-1); router.go(-1);
} }
} }
Vue.prototype.$navigation = navigationService;
app.$navigation = navigationService;
app.config.globalProperties.$navigation = navigationService;
} }
} }

View file

@ -1,8 +1,6 @@
import Vue from 'vue' import { createStore } from 'vuex'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist' import VuexPersistence from 'vuex-persist'
Vue.use(Vuex)
const USER = `convene_${ window.location.hostname }_user` const USER = `convene_${ window.location.hostname }_user`
const SETTINGS = `convene_${ window.location.hostname }_settings` const SETTINGS = `convene_${ window.location.hostname }_settings`
@ -67,7 +65,7 @@ const vuexPersistSessionStorage = new VuexPersistence({
const defaultUseSessionStorage = (sessionStorage.getItem(USER) != null); const defaultUseSessionStorage = (sessionStorage.getItem(USER) != null);
export default new Vuex.Store({ const store = createStore({
state: { language: null, currentRoomId: null, auth: null, tempuser: null, useLocalStorage: !defaultUseSessionStorage, globalNotification: false }, state: { language: null, currentRoomId: null, auth: null, tempuser: null, useLocalStorage: !defaultUseSessionStorage, globalNotification: false },
mutations: { mutations: {
loginSuccess(state, user) { loginSuccess(state, user) {
@ -144,4 +142,6 @@ export default new Vuex.Store({
} }
}, },
plugins: [vuexPersistLocalStorage.plugin, vuexPersistSessionStorage.plugin, persistUserPlugin] plugins: [vuexPersistLocalStorage.plugin, vuexPersistSessionStorage.plugin, persistUserPlugin]
}) });
export default store;

View file

@ -1,5 +1,7 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2"; import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import vitePluginVuetify from "vite-plugin-vuetify";
import { fileURLToPath, URL } from "node:url"; import { fileURLToPath, URL } from "node:url";
import Components from "unplugin-vue-components/vite"; import Components from "unplugin-vue-components/vite";
import { viteStaticCopy } from 'vite-plugin-static-copy'; import { viteStaticCopy } from 'vite-plugin-static-copy';
@ -11,8 +13,9 @@ function VuetifyResolver() {
return { return {
type: 'component', type: 'component',
resolve: (name) => { resolve: (name) => {
console.log("rESOLVE", name);
if (name.match(/^V[A-Z]/) && !name.includes("VEmojiPicker")) if (name.match(/^V[A-Z]/) && !name.includes("VEmojiPicker"))
return { name, from: 'vuetify/lib' } return { name, from: './node_modules/vuetify/lib/components' }
}, },
} }
} }
@ -28,9 +31,11 @@ export default defineConfig(({mode}) => ({
// exclude: ["*vuex-persist*", "*deepmerge*"] // exclude: ["*vuex-persist*", "*deepmerge*"]
// }), // }),
vue(), vue(),
Components({ vueJsx(),
resolvers: [VuetifyResolver()], vitePluginVuetify(),
}), // Components({
// resolvers: [VuetifyResolver()],
// }),
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [
{ {
@ -54,7 +59,8 @@ export default defineConfig(({mode}) => ({
alias: [ alias: [
{ find: "@", replacement: fileURLToPath(new URL("./src", import.meta.url)) }, { find: "@", replacement: fileURLToPath(new URL("./src", import.meta.url)) },
{ find: "~@", replacement: fileURLToPath(new URL("./src", import.meta.url)) }, { find: "~@", replacement: fileURLToPath(new URL("./src", import.meta.url)) },
{ find: "~vuetify", replacement: fileURLToPath(new URL("./node_modules/vuetify", import.meta.url)) }, { find: "~vuetify/src", replacement: fileURLToPath(new URL("./node_modules/vuetify/lib", import.meta.url)) },
{ find: "vue", replacement: fileURLToPath(new URL("./node_modules/vue/dist/vue.esm-bundler.js", import.meta.url)) },
], ],
}, },
define: { define: {