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" />
<link rel="icon" id="favicon" href="/favicon.ico" />
<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-96x96.png" sizes="96x96" />
<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>
</div>
</div>
<div id="app2"></div>
<!-- built files will be auto injected -->
</body>

2001
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
},
"dependencies": {
"@matrix-org/olm": "^3.2.12",
"@vitejs/plugin-vue2": "^2.3.3",
"@vitejs/plugin-vue": "^5.2.3",
"aes-js": "^3.1.2",
"axios": "^1.4.0",
"browserify-fs": "^1.0.0",
@ -40,21 +40,23 @@
"roboto-fontface": "*",
"stream-browserify": "^3.0.0",
"util": "^0.12.5",
"v-emoji-picker": "^2.3.1",
"vue": "^2.7.16",
"vue": "^3.5.13",
"vue-3-sanitize": "^0.1.4",
"vue-clipboard2": "^0.3.3",
"vue-i18n": "^8.28.2",
"vue-resize": "^1.0",
"vue-router": "^3.6.5",
"vue-sanitize": "^0.2.3",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.1",
"vue-swipeable-bottom-sheet": "^0.0.5",
"vuetify": "^2.7.2",
"vuex": "^3.5.1",
"vue3-emoji-picker": "^1.1.8",
"vue3-resize": "^0.2.0",
"vuetify": "^3.8.3",
"vuex": "^4.1.0",
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.3",
"@types/jszip": "^3.4.0",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vue/compiler-sfc": "^3.5.13",
"babel-eslint": "^10.1.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^7.0",
@ -65,9 +67,9 @@
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.2",
"vite-plugin-static-copy": "^2.3.0",
"vite-plugin-vuetify": "^2.1.1",
"vue-cli-plugin-vuetify": "^2.5.8",
"vue-template-compiler": "^2.7.16",
"vuetify-loader": "^1.3.0"
"vue-template-compiler": "^2.7.16"
},
"eslintConfig": {
"root": true,

View file

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

View file

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

View file

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

View file

@ -43,12 +43,10 @@
member.name.substring(0, 1).toUpperCase()
}}</span>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ memberName(member) }}</v-list-item-title>
<v-list-item-subtitle
v-text="member.userId"
></v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn icon v-if="active">
<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">
<template slot="label">
<i18n path="join.accept_ua" tag="span">
<i18n-t keypath="join.accept_ua" tag="span">
<template v-slot:agreement>
<a href="./ua.html" target="_blank" @click.stop>{{ $t("join.ua") }}</a>
</template>
</i18n>
</i18n-t>
</template>
</v-checkbox>

View file

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

View file

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

View file

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

View file

@ -50,18 +50,18 @@
v-if="$matrix.currentUser.is_guest"
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>
<b>{{ displayName }}</b>
</template>
</i18n>
</i18n-t>
</div>
<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>
<b>{{ displayName }}</b>
</template>
</i18n>
</i18n-t>
</div>
<v-avatar
class="avatar-32 d-inline-block"

View file

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

View file

@ -112,9 +112,7 @@
<v-list-item-avatar>
<v-icon color="black">{{ item.icon }}</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.text"></v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn icon v-if="active">
<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-text v-if="canViewRetentionPolicy">
<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-img
contain
@ -151,9 +149,7 @@
src="@/assets/icons/timer.svg"
/>
</v-list-item-avatar>
<v-list-item-content class="pb-0">
{{ messageRetentionDisplay }}
</v-list-item-content>
<v-list-item-action class="pb-0 mb-0">
<v-icon>arrow_drop_down</v-icon>

View file

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

View file

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

View file

@ -25,7 +25,7 @@
>
<v-list>
<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-avatar size="40" color="grey">
<AuthedImage v-if="memberAvatar(member.roomMember)" :src="memberAvatar(member.roomMember)" />
@ -34,10 +34,8 @@
}}</span>
</v-avatar>
</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-subtitle>{{ seenByTimeStamp(member.readTimestamp) }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</BottomSheet>

View file

@ -3,27 +3,19 @@
<v-list dense>
<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-content>
<v-list-item-title>{{ $t("menu.edit") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<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-content>
<v-list-item-title>{{ $t("menu.pin") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<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-content>
<v-list-item-title>{{ $t("menu.unpin") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<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-content>
<v-list-item-title>{{ $t("menu.delete") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</div>

View file

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

View file

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

View file

@ -7,11 +7,11 @@
</a>
</div>
<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>
<b>{{ messageRetentionDisplay }}</b>
</template>
</i18n>
</i18n-t>
</div>
</div>
</template>

View file

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

View file

@ -1,4 +1,3 @@
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import i18n from './plugins/lang';
@ -10,40 +9,44 @@ import analytics from './services/analytics.service'
import audioPlayer from './services/audio.service';
import 'roboto-fontface/css/roboto/roboto-fontface.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'
import VueResize from 'vue-resize';
import 'vue-resize/dist/vue-resize.css';
import VueResize from 'vue3-resize';
import 'vue3-resize/dist/vue3-resize.css';
import VueClipboard from 'vue-clipboard2'
import VueSanitize from "vue-sanitize";
import Vue3Sanitize from "vue-3-sanitize";
import createVuetify from './plugins/vuetify';
import { Buffer } from 'buffer/'
import { createApp, h } from 'vue';
globalThis.Buffer = Buffer;
var defaultOptions = VueSanitize.defaults;
var defaultOptions = Vue3Sanitize.defaults;
defaultOptions.disallowedTagsMode = "recursiveEscape";
defaultOptions.allowedTags = [];
Vue.use(VueSanitize, defaultOptions);
Vue.config.productionTip = false
const app = createApp({
render: () => h(App)
});
app.use(Vue3Sanitize, defaultOptions);
Vue.use(VueResize);
Vue.use(matrix, { store: store, i18n: i18n });
app.config.productionTip = false
app.use(VueResize);
const configLoadedPromise = new Promise((resolve, ignoredreject) => {
// eslint-disable-next-line
Vue.use(config, globalThis.window.location.origin, (config) => {
app.use(config, globalThis.window.location.origin, (config) => {
resolve(config);
}); // Use this before cleaninsights below, it depends on config!
});
Vue.use(analytics);
Vue.use(VueClipboard);
Vue.use(audioPlayer);
app.use(analytics);
app.use(VueClipboard);
app.use(audioPlayer);
const vuetify = createVuetify(config);
// Add bubble functionality to custom events.
// From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events
Vue.use((Vue) => {
Vue.prototype.$bubble = function $bubble(eventName, ...args) {
app.use((instance) => {
instance.$bubble = function $bubble(eventName, ...args) {
// Emit the event on all parent components
let component = this;
let arg = args.at(0);
@ -62,7 +65,7 @@ Vue.use((Vue) => {
});
// Register a global custom directive called `v-blur` that prevents focus
Vue.directive('blur', {
app.directive('blur', {
inserted: function (el) {
el.onfocus = (ev) => ev.target.blur()
}
@ -75,7 +78,7 @@ Vue.directive('blur', {
*
* Like this: v-linkTap:500="[tapped,longTapped]"
*/
Vue.directive('longTap', {
app.directive('longTap', {
bind: function (el, binding, ignoredvnode) {
el.longTapTimeout = parseInt(binding.arg || "500");
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({
vuetify,
store,
i18n,
router,
matrix,
config,
analytics,
audioPlayer,
render: h => h(App),
});
if (vueInstance.$config.accentColor) {
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
app.use(router);
router.app = app;
app.use(navigation, router);
app.use(vuetify);
app.use(i18n);
app.use(matrix, { store: store, i18n: i18n });
//app.use(matrix);
//app.use(config);
// app.use(analytics);
// 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) => {
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 VueI18n from 'vue-i18n'
Vue.use(VueI18n)
import { createI18n } from 'vue-i18n'
var messages = {}
@ -13,9 +10,10 @@ Object.keys(modules).map(path => {
messages[locale] = modules[path];
});
const vue18n = new VueI18n({
const vue18n = new createI18n({
locale: 'en',
fallbackLocale: 'en',
globalInjection: true,
silentFallbackWarn: true,
messages: messages,
pluralizationRules: {

View file

@ -1,5 +1,4 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
import {createVuetify } from "vuetify";
// Import all .vue icons and process them, so they can be used
// as $vuetify.icons.<iconname>
@ -12,11 +11,8 @@ Object.keys(modules).map(path => {
icons[iconName] = { component: modules[path].default }
});
Vue.use(Vuetify);
export default function(ignoredconfig) {
return new Vuetify({
return createVuetify({
icons: {
iconfont: 'md',
values: icons,

View file

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

View file

@ -2,8 +2,8 @@ import cleaninsights from './cleaninsights.service'
import matomo from './matomo.service'
export default {
install(Vue) {
const analyticsService = new Vue({
install(app) {
const analyticsService = ({
data() {
return {
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).
*/
export default {
install(Vue) {
install(app) {
class SharedAudioPlayer {
constructor() {
this.player = new Audio();
@ -34,12 +34,12 @@ export default {
// Listeners is just a Set of component "uid" entries for now.
entry = { url: null, listeners: new Set() };
// Make these reactive, so AudioPlayer (and others) can listen to them
Vue.set(entry, "loading", false);
Vue.set(entry, "loadPercent", 0);
Vue.set(entry, "duration", 0);
Vue.set(entry, "currentTime", 0);
Vue.set(entry, "playPercent", 0);
Vue.set(entry, "playing", false);
entry["loading"] = false;
entry["loadPercent"] = 0;
entry["duration"] = 0;
entry["currentTime"] = 0;
entry["playPercent"] = 0;
entry["playing"] = false;
this.infoMap.set(eventId, entry);
// 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";
export default {
install(Vue, defaultServerFromLocation, onloaded) {
var config = Vue.observable(defaultConfig.default);
Vue.set(config, "loaded", false);
install(app, defaultServerFromLocation, onloaded) {
var config = defaultConfig.default;
config["loaded"] = false;
const getRuntimeConfig = () => {
return fetch('./config.json?ms=' + Date.now()).then((res) => res.json()).catch(err => {
console.error("Failed to get config:", err);
@ -14,25 +14,25 @@ export default {
config.promise = getRuntimeConfig().then((json) => {
// Reactively use all the config values
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 (!json.defaultBaseUrl) {
if (json.defaultServer) {
// TODO - Only to migrate old values (defaultServer was renamed defaultBaseUrl), can be removed later...
Vue.set(config, "defaultBaseUrl", defaultServerFromLocation);
config["defaultBaseUrl"] = defaultServerFromLocation;
} else {
Vue.set(config, "defaultBaseUrl", json.defaultServer);
config["defaultBaseUrl"] = json.defaultServer;
}
}
if (json.useFullyQualifiedDMLinks == undefined) {
Vue.set(config, "useFullyQualifiedDMLinks", true); // Default to true
config["useFullyQualifiedDMLinks"] = true; // Default to true
}
if (json.disableMediaSharing == undefined) {
Vue.set(config, "disableMediaSharing", false);
}
if (!json.maxSizeAutoDownloads) {
Vue.set(config, "maxSizeAutoDownloads", 10 * 1024 * 1024);
config["maxSizeAutoDownloads"] = 10 * 1024 * 1024;
}
if (!json.roomTypes) {
let roomTypes = ["group_chat", "channel"];
@ -40,9 +40,9 @@ export default {
if (fileDropEnabled) {
roomTypes.push("file_drop");
}
Vue.set(config, "roomTypes", roomTypes);
config["roomTypes"] = roomTypes;
}
Vue.set(config, "loaded", true);
config["loaded"] = true;
document.title = config.appName || "";
@ -70,6 +70,7 @@ export default {
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 { 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 { TimelineWindow, EventTimeline } from "matrix-js-sdk";
import util from "../plugins/utils";
import User from "../models/user";
import * as LocalStorageCryptoStoreClass from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store";
const LocalStorageCryptoStore = LocalStorageCryptoStoreClass.LocalStorageCryptoStore;
import { LocalStorageCryptoStore } from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store";
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
@ -16,11 +17,13 @@ export const CHANNEL_POWER_LEVELS = {
};
export default {
install(Vue, options) {
install(app, options) {
if (!options || !options.store) {
throw new Error("Please initialise plugin with a Vuex store.");
}
const store = options.store;
// Set User-Agent headers.
// 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...
@ -30,12 +33,7 @@ export default {
// var ret = orig(opts, callback);
// return ret;
// });
const store = options.store;
const i18n = options.i18n;
const matrixService = new Vue({
store,
i18n,
const matrixService = createApp({
data() {
return {
matrixClient: null,
@ -44,13 +42,10 @@ export default {
userDisplayName: null,
userAvatar: null,
currentRoom: null,
userCanSendMessageInCurrentRoom: true,
userCanSendReactionAndAnswerPollInCurrentRoom: true,
currentRoomBeingPurged: false,
notificationCount: 0,
useAuthedMedia: false,
};
},
mounted() {
console.log("Matrix service mounted");
},
@ -77,8 +72,8 @@ export default {
return null;
},
currentUserMXDomain() {
return User.domainPart(this.currentUserId) || this.$config.defaultMatrixDomainPart;
currentUserHomeServer() {
return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId);
},
currentRoomId() {
@ -96,12 +91,6 @@ export default {
return room.selfMembership === "invite";
});
},
joinedAndInvitedRooms() {
return this.rooms.filter((room) => {
return room.selfMembership === "join" || room.selfMembership === "invite";
});
},
},
watch: {
@ -111,18 +100,6 @@ export default {
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: {
@ -130,11 +107,9 @@ export default {
console.log("create crypto store");
return new LocalStorageCryptoStore(this.$store.getters.storage);
},
login(user, registrationFlowHandler, createUser = false) {
return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
login(user) {
const tempMatrixClient = sdk.createClient({
baseUrl: baseUrl,
idBaseUrl: this.$config.identityServer,
baseUrl: user.home_server,
});
var promiseLogin;
@ -142,52 +117,27 @@ export default {
if (user.access_token) {
// Logged in on "real" account
promiseLogin = Promise.resolve(user);
} else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) {
} else if (user.is_guest && !user.user_id) {
// Generate random username and password. We don't user REAL matrix
// guest accounts because 1. They are not allowed to post media, 2. They
// can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
//
// Instead, we use an ILAG approach, Improved Landing as Guest.
const userId =
createUser || user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix);
const pass = createUser || user.registration_session ? user.password : util.randomPass();
const extractAndSaveUser = (response) => {
var u = Object.assign({}, response);
u.password = pass;
u.is_guest = true;
this.$store.commit("setUser", u);
return u;
};
const user = util.randomUser(this.$config.userIdPrefix);
const pass = util.randomPass();
promiseLogin = tempMatrixClient
.register(userId, pass, user.registration_session || null, {
.register(user, pass, null, {
type: "m.login.dummy",
initial_device_display_name: this.$config.appName,
})
.then((response) => {
return extractAndSaveUser(response);
})
.catch((error) => {
if (registrationFlowHandler && error.httpStatus == 401 && error.data) {
const registrationSession = error.data.session;
// Store user, pass and session, so we can resume if network failure occurs etc.
//
var u = {};
u.user_id = userId;
console.log("Response", response);
var u = Object.assign({}, response);
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
u.password = pass;
u.is_guest = true;
u.registration_session = registrationSession;
this.$store.commit("setUser", u);
return registrationFlowHandler(tempMatrixClient, error.data).then((response) =>
extractAndSaveUser(response)
);
} else {
console.error(error);
}
throw error;
return u;
});
} else {
var data = {
@ -205,6 +155,7 @@ export default {
// Copy over needed properties
u = Object.assign(user, response);
}
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
this.$store.commit("setUser", u);
return u;
});
@ -213,7 +164,6 @@ export default {
return promiseLogin.then((user) => {
return self.getMatrixClient(user);
});
});
},
clearCryptoStore() {
@ -287,25 +237,22 @@ export default {
const matrixStore = new sdk.MemoryStore(this.$store.getters.storage);
return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
var opts = {
baseUrl: baseUrl,
baseUrl: user.home_server,
userId: user.user_id,
store: matrixStore,
deviceId: user.device_id,
accessToken: user.access_token,
timelineSupport: true,
unstableClientRelationAggregation: true,
cryptoStore: this.createCryptoStore()
//useAuthorizationHeader: true
};
this.matrixClient = sdk.createClient(opts);
// if (user.is_guest) {
// this.matrixClient.setGuest(true);
// }
console.error("Created client", this.matrixClient);
return this.matrixClient
.initRustCrypto()
.initCrypto()
.then(() => {
console.log("Crypto initialized");
@ -332,16 +279,10 @@ export default {
}
})
.then(() => {
return this.matrixClient.isVersionSupported("v1.11");
})
.then((authedMediaSupported) => {
this.useAuthedMedia = authedMediaSupported;
// Ready to use! Start by loading rooms.
this.initClient();
return user;
});
});
},
/**
@ -350,14 +291,11 @@ export default {
* Will use a real account, if we have one, otherwise will create
* a random account.
*/
getLoginPromise(registrationFlowHandler) {
getLoginPromise() {
if (this.ready) {
return Promise.resolve(this.currentUser);
}
return this.$store.dispatch("login", {
user: this.currentUser || new User("", "", true),
registrationFlowHandler,
});
return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true));
},
addMatrixClientListeners(client) {
@ -385,7 +323,7 @@ export default {
{
const room = this.matrixClient.getRoom(event.getRoomId());
if (room) {
Vue.set(room, "topic", event.getContent().topic);
room.topic = event.getContent().topic;
}
}
break;
@ -394,56 +332,24 @@ export default {
{
const room = this.matrixClient.getRoom(event.getRoomId());
if (room) {
Vue.set(
room,
"avatar",
room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia)
);
room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true);
}
}
break;
case "m.room.power_levels":
case "m.room.member":
{
if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(event.getRoomId(), this.currentUserId);
this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(event.getRoomId(), this.currentUserId);
}
}
break;
case "m.room.join_rules":
{
const room = this.matrixClient.getRoom(event.getRoomId());
if (room && room.getJoinRule() == "private" && room.selfMembership == "invite") {
// We have an invite to a room that's now "private"? This is most probably a deleted DM room.
// Reject the invite, i.e. call "leave" on it.
this.matrixClient.leave(room.roomId);
}
}
break;
case "m.room.canonical_alias":
{
if (this.currentRoomId && this.currentRoomId.startsWith("#") && !this.currentRoom) {
this.currentRoom = this.getRoom(this.currentRoomId);
}
}
break;
case STATE_EVENT_ROOM_DELETED:
{
const room = this.matrixClient.getRoom(event.getRoomId());
if (room && room.currentState) {
// Before we do anything, make sure the sender is an admin!
// Also, do not react if WE are the sender, since we are probably
// busy doing the rest of the purging process...
if (room.currentState.maySendStateEvent("m.room.power_levels", event.getSender())) {
if (event.getSender() !== this.currentUserId) {
this.leaveRoomAndNavigate(room.roomId).then(() => {
this.matrixClient.forget(room.roomId, true);
});
}
// Don't use this.currentRoomId, may be an alias. We need the real id!
if (
event.getContent().membership == "leave" &&
(event.getPrevContent() || {}).membership == "join" &&
event.getStateKey() == this.currentUserId &&
event.getSender() != this.currentUserId
) {
// We were kicked
const wasPurged = event.getContent().reason == "Room Deleted";
this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
}
}
}
@ -452,19 +358,8 @@ export default {
this.updateNotificationCount();
},
onRoom(room) {
if (room.selfMembership === "invite") {
this.matrixClient
.getRoomTags(room.roomId)
.then((reply) => {
if (Object.keys(reply.tags).includes("m.server_notice")) {
Vue.set(room, "isServiceNoticeRoom", true);
}
})
.catch((error) => {
console.error(error);
});
}
onRoom(ignoredroom) {
console.log("Got room", ignoredroom);
this.reloadRooms();
this.updateNotificationCount();
},
@ -519,25 +414,19 @@ export default {
// each time!
var updatedRooms = this.matrixClient.getVisibleRooms();
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) => {
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);
const resolvedId =
this.currentRoomId && this.currentRoomId.startsWith("#")
? this.matrixClient.getRoomIdForAlias(this.currentRoomId).then((r) => r.room_id)
: Promise.resolve(this.currentRoomId);
resolvedId
.then((roomId) => {
const currentRoom = this.getRoom(roomId);
console.log("Reload rooms", updatedRooms);
this.rooms = updatedRooms;
const currentRoom = this.getRoom(this.$store.state.currentRoomId);
if (this.currentRoom != currentRoom) {
this.currentRoom = currentRoom;
}
})
.catch((ignorederror) => {});
},
setCurrentRoomId(roomId) {
@ -603,178 +492,15 @@ export default {
},
leaveRoom(roomId) {
return this.matrixClient.leave(roomId).then(() => {
this.$store.commit("setCurrentRoomId", null);
return this.matrixClient.leave(roomId, undefined).then(() => {
this.rooms = this.rooms.filter((room) => {
room.roomId != roomId;
});
//this.matrixClient.store.removeRoom(roomId);
this.matrixClient.store.removeRoom(roomId);
//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:
* - Make room invite only
@ -785,30 +511,6 @@ export default {
* @param roomId
*/
purgeRoom(roomId, statusCallback) {
this.currentRoomBeingPurged = true;
//console.log("Purge room");
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const withRetry = (codeBlock) => {
return codeBlock().catch((error) => {
if (error && error.errcode == "M_LIMIT_EXCEEDED") {
var retry = 1000;
if (error.data) {
const retryIn = error.data.retry_after_ms;
retry = Math.max(retry, retryIn ? retryIn : 0);
}
console.log("Rate limited, retry in", retry);
return sleep(retry).then(() => withRetry(codeBlock));
} else {
return Promise.resolve();
}
});
};
const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices();
return new Promise((resolve, reject) => {
const room = this.getRoom(roomId);
@ -817,39 +519,22 @@ export default {
return;
}
// Remove any possible pending events
room.getLiveTimeline().getEvents().filter((e) => [EventStatus.ENCRYPTING, sdk.EventStatus.QUEUED].includes(e.status)).forEach((e) => {
//console.log("Cancel pending event!");
this.matrixClient.cancelPendingEvent(e);
});
const timelineWindow = new TimelineWindow(this.matrixClient, room.getUnfilteredTimelineSet(), {});
const self = this;
//console.log("Purge: set invite only");
statusCallback(this.$t("room.purge_set_room_state"));
withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "private" }, ""))
this.matrixClient
.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "invite" }, "")
.then(() => {
//console.log("Purge: forbid guest access");
return withRetry(() => this.matrixClient.sendStateEvent(
roomId,
"m.room.guest_access",
{ guest_access: "forbidden" },
""
));
return this.matrixClient.sendStateEvent(roomId, "m.room.guest_access", { guest_access: "forbidden" }, "");
})
.then(() => {
//console.log("Purge: set history visibility to 'joined'");
return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", {
return this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", {
history_visibility: "joined",
}));
})
.then(() => {
return withRetry(() => this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" }));
})
.then(() => {
// Set message retention 1 minute (may be limited by server)
return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.retention", { max_lifetime: 60000 }));
});
})
.then(() => {
//console.log("Purge: create timeline");
@ -859,11 +544,8 @@ export default {
const getMoreIfAvailable = function _getMoreIfAvailable() {
if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) {
//console.log("Purge: page back");
return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((gotmore) => {
if (gotmore) {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((ignoredsuccess) => {
return _getMoreIfAvailable.call(self);
}
return Promise.resolve("Done");
});
} else {
return Promise.resolve("Done");
@ -876,92 +558,40 @@ export default {
statusCallback(this.$t("room.purge_redacting_events"));
// First ignore unknown device errors
this.matrixClient.setGlobalErrorOnUnknownDevices(false);
const allEvents = timelineWindow.getEvents().filter((event) => {
return (
!event.isRedacted() &&
!event.isRedaction() &&
!event.isState() &&
(!room.currentState || room.currentState.maySendRedactionForEvent(event, this.currentUserId))
);
});
const redactFirstEvent = (events) => {
statusCallback(
this.$t("room.purge_redacting_events", {
count: allEvents.length - events.length + 1,
total: allEvents.length,
})
);
if (events.length == 0) {
return Promise.resolve(true);
var redactionPromises = [];
timelineWindow.getEvents().forEach((event) => {
if (!event.isRedacted() && !event.isRedaction() && !event.isState()) {
// Redact!
redactionPromises.push(this.matrixClient.redactEvent(event.getRoomId(), event.getId()));
}
const event = events[0];
return withRetry(() => this.matrixClient.redactEvent(event.getRoomId(), event.getId())).then(() =>
redactFirstEvent(events.slice(1))
);
};
return redactFirstEvent(allEvents);
});
return Promise.all(redactionPromises);
})
.then(() => {
//console.log("Purge: kick members");
statusCallback(this.$t("room.purge_removing_members"));
var joined = room.getMembersWithMembership("join");
var invited = room.getMembersWithMembership("invite");
var allMembers = joined.concat(invited);
var members = joined.concat(invited);
const me = allMembers.find((m) => m.userId == self.currentUserId);
const kickFirstMember = (members) => {
//console.log(`Kicking ${members.length} members`);
statusCallback(
this.$t("room.purge_removing_members", {
count: allMembers.length - members.length + 1,
total: allMembers.length,
})
);
if (members.length == 0) {
return Promise.resolve(true);
var kickPromises = [];
members.forEach((member) => {
if (member.userId != self.currentUserId) {
kickPromises.push(this.matrixClient.kick(roomId, member.userId, "Room Deleted"));
}
const member = members[0];
if (member.userId == self.currentUserId) {
return kickFirstMember(members.slice(1));
} else {
// Slight pause to avoid rate limiting.
return sleep(0.1)
.then(() => withRetry(() => {
if (member.membership == "invite" && me && me.powerLevel <= member.powerLevel) {
// The user is invited, but we can't kick them because of power levels.
// Send a new invite with reason set to "Room Deleted".
// The client will be sent stripped room state, and can from that see the
// join_rule of "private". It will then "leave", i.e. reject the invite.
return this.matrixClient.invite(roomId, member.userId, "Room Deleted");
}
return this.matrixClient.kick(roomId, member.userId, "Room Deleted")
}))
.then(() => kickFirstMember(members.slice(1)));
}
};
return kickFirstMember(allMembers);
})
.then(() => {
return withRetry(() =>
this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" })
);
});
return Promise.all(kickPromises);
})
.then(() => {
statusCallback(null);
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
return withRetry(() => this.leaveRoom(roomId));
return this.leaveRoom(roomId);
})
.then(() => {
this.currentRoomBeingPurged = false;
resolve(true); // Done!
})
.catch((err) => {
console.error("Error purging room", err);
this.currentRoomBeingPurged = false;
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
reject(err);
});
@ -1017,17 +647,7 @@ export default {
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "invited",
},
},
{
type: "m.room.power_levels",
state_key: "",
content: {
users: {
[this.currentUserId]: 100,
[userId]: 100,
},
history_visibility: "joined",
},
},
],
@ -1050,7 +670,7 @@ export default {
* @param {*} 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);
if (other) {
if (room.getMyMembership() == "invite" && other.membership == "join") {
@ -1065,20 +685,6 @@ export default {
return false;
},
/**
* Return true if this room is a direct room with one other user. NOTE: this currently
* only checks number of members, not any is_direct flag.
* @param { } room
*/
isDirectRoom(room) {
// TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
// apprently need to set this...
if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) {
return true;
}
return false;
},
on(event, handler) {
if (this.matrixClient) {
this.matrixClient.on(event, handler);
@ -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) {
if (this.matrixClient && this.currentUser) {
const authDict = {
@ -1135,26 +730,31 @@ export default {
return this.matrixClient.uploadContent(file, opts);
},
/**
* Get a matrix client that can be used for public queries. If we are logged in, this is the normal
* matrix client. If not, we create a temp one with a temp password.
* @returns A MatrixClient that can be used for public queries
*/
getPublicQueryMatrixClient() {
getPublicRoomInfo(roomId) {
if (!roomId) {
return Promise.reject("Invalid parameters");
}
const parts = roomId.split(":");
if (parts.length != 2) {
return Promise.reject("Unknown room server");
}
const server = parts[1];
var clientPromise;
if (this.matrixClient) {
return this.getMatrixClient().then(() => {
clientPromise = this.getMatrixClient().then(() => {
return this.matrixClient;
});
} else {
const tempMatrixClient = sdk.createClient({
baseUrl: this.$config.defaultServer,
});
var tempUserString = this.$store.state.tempuser;
var tempUser = null;
if (tempUserString) {
tempUser = JSON.parse(tempUserString);
}
return util.getMatrixBaseUrl(tempUser, this.$config).then((baseUrl) => {
const tempMatrixClient = sdk.createClient({ baseUrl: baseUrl });
var clientPromise;
// Need to create an account?
//
@ -1197,7 +797,7 @@ export default {
// Only used to get public room info from.
clientPromise = clientPromise.then((user) => {
var opts = {
baseUrl: baseUrl,
baseUrl: this.$config.defaultServer,
userId: user.user_id,
accessToken: user.access_token,
timelineSupport: false,
@ -1206,33 +806,16 @@ export default {
matrixClient.startClient();
return matrixClient;
});
return clientPromise;
});
}
},
getPublicRoomInfo(roomId) {
if (!roomId) {
return Promise.reject("Invalid parameters");
}
const parts = roomId.split(":");
if (parts.length != 2) {
return Promise.reject("Unknown room server");
}
const server = parts[1];
const clientPromise = this.getPublicQueryMatrixClient();
const findOrGetMore = function _findOrGetMore(client, useAuthedMedia, response) {
const findOrGetMore = function _findOrGetMore(client, response) {
for (var room of response.chunk) {
if (
(roomId.startsWith("#") && room.canonical_alias == roomId) ||
(roomId.startsWith("!") && room.room_id == roomId)
) {
if (room.avatar_url) {
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);
}
@ -1256,18 +839,13 @@ export default {
};
var matrixClient;
let useAuthedMedia = false;
return clientPromise
.then((client) => {
matrixClient = client;
return client.isVersionSupported("v1.11");
})
.then((version1_11) => {
useAuthedMedia = version1_11;
return matrixClient.publicRooms({ server: server, limit: 1000 });
})
.then((response) => {
return findOrGetMore(matrixClient, useAuthedMedia, response);
return findOrGetMore(matrixClient, response);
})
.catch((err) => {
return Promise.reject("Failed to find room: " + err);
@ -1281,38 +859,20 @@ export default {
});
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: () => {}
});
sdk.setCryptoStoreFactory(matrixService.createCryptoStore.bind(matrixService));
Vue.prototype.$matrix = matrixService;
matrixService.config.globalProperties.$store = store;
matrixService.config.globalProperties.$config = app.$config;
matrixService.config.globalProperties.$t = app.$t;
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 {
install(Vue, router) {
install(app, router) {
var routes = [];
var nextRoutes = null;
var zeroIndex = undefined;
@ -75,6 +75,8 @@ export default {
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 Vuex from 'vuex'
import { createStore } from 'vuex'
import VuexPersistence from 'vuex-persist'
Vue.use(Vuex)
const USER = `convene_${ window.location.hostname }_user`
const SETTINGS = `convene_${ window.location.hostname }_settings`
@ -67,7 +65,7 @@ const vuexPersistSessionStorage = new VuexPersistence({
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 },
mutations: {
loginSuccess(state, user) {
@ -144,4 +142,6 @@ export default new Vuex.Store({
}
},
plugins: [vuexPersistLocalStorage.plugin, vuexPersistSessionStorage.plugin, persistUserPlugin]
})
});
export default store;

View file

@ -1,5 +1,7 @@
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 Components from "unplugin-vue-components/vite";
import { viteStaticCopy } from 'vite-plugin-static-copy';
@ -11,8 +13,9 @@ function VuetifyResolver() {
return {
type: 'component',
resolve: (name) => {
console.log("rESOLVE", name);
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*"]
// }),
vue(),
Components({
resolvers: [VuetifyResolver()],
}),
vueJsx(),
vitePluginVuetify(),
// Components({
// resolvers: [VuetifyResolver()],
// }),
viteStaticCopy({
targets: [
{
@ -54,7 +59,8 @@ export default defineConfig(({mode}) => ({
alias: [
{ 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: {