Support upgraded rooms (via links to successor/predecessor)

This commit is contained in:
N-Pex 2025-08-04 09:44:06 +02:00
parent 970f82ba29
commit 615aa2b781
11 changed files with 181 additions and 48 deletions

View file

@ -115,7 +115,10 @@
"images": "Images", "images": "Images",
"send_attachements_dialog_title": "Do you want to send following attachments?", "send_attachements_dialog_title": "Do you want to send following attachments?",
"download_all": "Download all", "download_all": "Download all",
"failed_to_render": "Failed to render event" "failed_to_render": "Failed to render event",
"room_upgraded": "This room has been upgraded, go {link} to rejoin the discussion",
"room_upgraded_link": "here",
"room_upgraded_view_old": "This room was upgraded. Click {link} to view old messages"
}, },
"room": { "room": {
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations", "invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",

View file

@ -242,7 +242,7 @@
:title="room.name" :title="room.name"
/> />
<MessageOperationsBottomSheet ref="messageOperationsSheet"> <BottomSheet ref="messageOperationsSheet" halfY="0.1">
<EmojiPicker ref="emojiPicker" <EmojiPicker ref="emojiPicker"
:native="true" :native="true"
@select="emojiSelected" @select="emojiSelected"
@ -252,7 +252,7 @@
:group-order="['recently_used']" :group-order="['recently_used']"
disable-skin-tones disable-skin-tones
:static-texts="{ placeholder: $t('emoji.search')}"/> :static-texts="{ placeholder: $t('emoji.search')}"/>
</MessageOperationsBottomSheet> </BottomSheet>
<StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" /> <StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />
@ -316,7 +316,6 @@ import WelcomeHeaderDirectChat from "./welcome_headers/WelcomeHeaderDirectChat";
import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel"; import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel";
import WelcomeHeaderChannelUser from "./welcome_headers/WelcomeHeaderChannelUser"; import WelcomeHeaderChannelUser from "./welcome_headers/WelcomeHeaderChannelUser";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue"; import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet"; import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
import UserProfileDialog from "./UserProfileDialog.vue" import UserProfileDialog from "./UserProfileDialog.vue"
import BottomSheet from "./BottomSheet.vue"; import BottomSheet from "./BottomSheet.vue";
@ -379,7 +378,6 @@ export default {
WelcomeHeaderRoom, WelcomeHeaderRoom,
WelcomeHeaderDirectChat, WelcomeHeaderDirectChat,
NoHistoryRoomWelcomeHeader, NoHistoryRoomWelcomeHeader,
MessageOperationsBottomSheet,
StickerPickerBottomSheet, StickerPickerBottomSheet,
BottomSheet, BottomSheet,
CreatePollDialog, CreatePollDialog,

View file

@ -78,7 +78,7 @@
</v-col> </v-col>
</v-row> </v-row>
<v-checkbox id="chk-accept-ua" class="mt-0" v-model="acceptUA"> <v-checkbox id="chk-accept-ua" class="mt-0" v-model="acceptUA" v-if="!$store.state.uaAccepted">
<template v-slot:label> <template v-slot:label>
<i18n-t keypath="join.accept_ua" tag="span"> <i18n-t keypath="join.accept_ua" tag="span">
<template v-slot:agreement> <template v-slot:agreement>
@ -265,6 +265,7 @@ export default {
if (!value || (value && value == oldVal)) { if (!value || (value && value == oldVal)) {
return; // No change. return; // No change.
} }
this.acceptUA = this.$store.state.uaAccepted;
console.log("Join: Current room changed to " + (value ? value : "null")); console.log("Join: Current room changed to " + (value ? value : "null"));
this.roomName = this.removeHomeServer(this.roomId); this.roomName = this.removeHomeServer(this.roomId);
@ -437,6 +438,7 @@ export default {
.then((room) => { .then((room) => {
this.loading = false; this.loading = false;
this.loadingMessage = null; this.loadingMessage = null;
this.$store.commit("acceptUA", true);
this.$nextTick(() => { this.$nextTick(() => {
if (this.roomNeedsKnock) { if (this.roomNeedsKnock) {
// For knocks, send to room list // For knocks, send to room list

View file

@ -75,7 +75,7 @@ import ContactJoin from "./messages/ContactJoin.vue";
import ContactLeave from "./messages/ContactLeave.vue"; import ContactLeave from "./messages/ContactLeave.vue";
import ContactInvited from "./messages/ContactInvited.vue"; import ContactInvited from "./messages/ContactInvited.vue";
import ContactChanged from "./messages/ContactChanged.vue"; import ContactChanged from "./messages/ContactChanged.vue";
import RoomCreated from "./messages/RoomCreated.vue"; import RoomCreated from "./messages/composition/RoomCreated.vue";
import RoomAliased from "./messages/RoomAliased.vue"; import RoomAliased from "./messages/RoomAliased.vue";
import RoomNameChanged from "./messages/RoomNameChanged.vue"; import RoomNameChanged from "./messages/RoomNameChanged.vue";
import RoomTopicChanged from "./messages/RoomTopicChanged.vue"; import RoomTopicChanged from "./messages/RoomTopicChanged.vue";

View file

@ -61,7 +61,7 @@
</v-list-item> </v-list-item>
<v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room.roomId" <v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room.roomId"
@click="currentRoomId = room.roomId" class="room-list-room"> v-on:click="() => goToRoom(room)" class="room-list-room">
<template v-slot:prepend> <template v-slot:prepend>
<v-avatar size="42" color="#d9d9d9" :class="[{ 'rounded-circle': isDirect(room) }]"> <v-avatar size="42" color="#d9d9d9" :class="[{ 'rounded-circle': isDirect(room) }]">
<AuthedImage v-if="roomAvatar(room)" :src="roomAvatar(room)" /> <AuthedImage v-if="roomAvatar(room)" :src="roomAvatar(room)" />
@ -87,7 +87,7 @@
</template> </template>
<script> <script>
import util from "../plugins/utils"; import utils from "../plugins/utils";
import AuthedImage from "./AuthedImage.vue"; import AuthedImage from "./AuthedImage.vue";
export default { export default {
@ -124,7 +124,19 @@ export default {
}, },
joinedRooms() { joinedRooms() {
// show room with notification on top, followed by room decending order by active Timestamp // show room with notification on top, followed by room decending order by active Timestamp
return [...this.$matrix.joinedRooms].sort((a, b) => { let rooms = this.$matrix.joinedRooms;
let upgradedRooms = rooms.filter((r) => r.currentState.getStateEvents("m.room.tombstone").length > 0);
let normalRooms = rooms.filter((r) => r.currentState.getStateEvents("m.room.tombstone").length == 0);
let normalRoomsId = normalRooms.map((r) => r.roomId);
upgradedRooms.forEach(r => {
const history = this.$matrix.matrixClient.getRoomUpgradeHistory(r.roomId, true, true);
const successor = history[history.length - 1];
if (!normalRoomsId.includes(successor.roomId)) {
normalRoomsId.push(successor.roomId);
normalRooms.push(successor);
}
});
return [...normalRooms].sort((a, b) => {
if (this.notificationCount(a)) return -1; if (this.notificationCount(a)) return -1;
if (this.notificationCount(b)) return 1; if (this.notificationCount(b)) return 1;
return b.getLastActiveTimestamp() - a.getLastActiveTimestamp() return b.getLastActiveTimestamp() - a.getLastActiveTimestamp()
@ -132,6 +144,25 @@ export default {
}, },
}, },
methods: { methods: {
goToRoom(room) {
const events = room.currentState.getStateEvents("m.room.tombstone");
if (events && events.length > 0) {
const replacement_room = events[events.length - 1].getContent().replacement_room;
this.$navigation.push(
{
name: "Join",
params: {
roomId: utils.sanitizeRoomId(replacement_room),
join: true
},
query: this.$route.query
},
0
);
} else {
this.currentRoomId = utils.sanitizeRoomId(room.roomId);
}
},
roomAvatar(room) { roomAvatar(room) {
if (this.isDirect(room)) { if (this.isDirect(room)) {
if (room.avatar) { if (room.avatar) {
@ -184,7 +215,7 @@ export default {
{ {
name: "Chat", name: "Chat",
params: { params: {
roomId: util.sanitizeRoomId( roomId: utils.sanitizeRoomId(
room.getCanonicalAlias() || room.roomId room.getCanonicalAlias() || room.roomId
), ),
}, },
@ -232,7 +263,7 @@ export default {
this.$navigation.push( this.$navigation.push(
{ {
name: "Chat", name: "Chat",
params: { roomId: util.sanitizeRoomId(this.currentRoomId) }, params: { roomId: utils.sanitizeRoomId(this.currentRoomId) },
}, },
-1 -1
); );

View file

@ -27,7 +27,7 @@ import ContactInvited from "./messages/ContactInvited.vue";
import ContactKicked from "./messages/ContactKicked.vue"; import ContactKicked from "./messages/ContactKicked.vue";
import ContactBanned from "./messages/ContactBanned.vue"; import ContactBanned from "./messages/ContactBanned.vue";
import ContactChanged from "./messages/ContactChanged.vue"; import ContactChanged from "./messages/ContactChanged.vue";
import RoomCreated from "./messages/RoomCreated.vue"; import RoomCreated from "./messages/composition/RoomCreated.vue";
import RoomAliased from "./messages/RoomAliased.vue"; import RoomAliased from "./messages/RoomAliased.vue";
import RoomNameChanged from "./messages/RoomNameChanged.vue"; import RoomNameChanged from "./messages/RoomNameChanged.vue";
import RoomTopicChanged from "./messages/RoomTopicChanged.vue"; import RoomTopicChanged from "./messages/RoomTopicChanged.vue";
@ -38,7 +38,6 @@ import ChatHeader from "./ChatHeader";
import VoiceRecorder from "./VoiceRecorder"; import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet"; import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom.vue"; import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import stickers from "../plugins/stickers"; import stickers from "../plugins/stickers";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet"; import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
import BottomSheet from "./BottomSheet.vue"; import BottomSheet from "./BottomSheet.vue";
@ -50,6 +49,7 @@ import RoomEncrypted from "./messages/RoomEncrypted.vue";
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue"; import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
import DebugEvent from "./messages/DebugEvent.vue"; import DebugEvent from "./messages/DebugEvent.vue";
import ReadMarker from "./messages/ReadMarker.vue"; import ReadMarker from "./messages/ReadMarker.vue";
import RoomTombstone from "./messages/composition/RoomTombstone.vue";
import roomDisplayOptionsMixin from "./roomDisplayOptionsMixin"; import roomDisplayOptionsMixin from "./roomDisplayOptionsMixin";
import roomTypeMixin from "./roomTypeMixin"; import roomTypeMixin from "./roomTypeMixin";
@ -92,11 +92,11 @@ export default {
VoiceRecorder, VoiceRecorder,
RoomInfoBottomSheet, RoomInfoBottomSheet,
WelcomeHeaderRoom, WelcomeHeaderRoom,
MessageOperationsBottomSheet,
StickerPickerBottomSheet, StickerPickerBottomSheet,
BottomSheet, BottomSheet,
CreatePollDialog, CreatePollDialog,
ReadMarker, ReadMarker,
RoomTombstone
}, },
computed: { computed: {
debugging() { debugging() {
@ -264,10 +264,7 @@ export default {
} }
case "m.room.create": case "m.room.create":
if (this.showAllStatusMessages) { return RoomCreated; // Check showAllStatusMessages in the component. We might want to show this always for upgraded rooms.
return RoomCreated;
}
break;
case "m.room.canonical_alias": case "m.room.canonical_alias":
if (this.showAllStatusMessages) { if (this.showAllStatusMessages) {
@ -323,6 +320,9 @@ export default {
} }
break; break;
case "m.room.tombstone":
return RoomTombstone;
case "m.poll.start": case "m.poll.start":
case "org.matrix.msc3381.poll.start": case "org.matrix.msc3381.poll.start":
if (event.getSender() != this.$matrix.currentUserId) { if (event.getSender() != this.$matrix.currentUserId) {

View file

@ -1,17 +0,0 @@
<template>
<div class="statusEvent">
{{ $t('message.user_created_room', {user: eventSenderDisplayName(event)}) }}
</div>
</template>
<script>
import messageMixin from "./messageMixin";
export default {
mixins: [messageMixin],
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -0,0 +1,69 @@
<template>
<div class="statusEvent" v-if="predecessorRoom !== undefined">
<i18n-t keypath="message.room_upgraded_view_old" tag="span">
<template v-slot:link>
<a v-on:click="goToPredecessor" class="clickable" href="about:blank">{{ $t("message.room_upgraded_link") }}</a>
</template>
</i18n-t>
</div>
<div class="statusEvent">
{{ $t('message.user_created_room', {user: eventSenderDisplayName(event)}) }}
</div>
</template>
<script setup lang="ts">
import { inject, ref, Ref, watch } from "vue";
import { MessageProps, useMessage } from "./useMessage";
import utils from "@/plugins/utils";
import { Room } from "matrix-js-sdk";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const $matrix: any = inject("globalMatrix");
const $navigation: any = inject("globalNavigation");
const props = defineProps<MessageProps>();
const predecessorRoom: Ref<Room | undefined> = ref(undefined);
const { event, eventSenderDisplayName } = useMessage(
$matrix,
t,
props,
undefined,
undefined
);
watch(props.room, (room) => {
const rooms = $matrix.matrixClient.getRoomUpgradeHistory(room.roomId, true, true);
if (rooms && rooms.length > 1) {
const idx = rooms.indexOf(props.room);
if (idx > 0) {
predecessorRoom.value = rooms[idx - 1];
return;
}
}
predecessorRoom.value = undefined;
}, { immediate: true });
const goToPredecessor = (e: Event) => {
e.preventDefault();
e.stopPropagation();
if (predecessorRoom.value) {
$navigation.push(
{
name: "Chat",
params: {
roomId: utils.sanitizeRoomId(predecessorRoom.value.roomId),
},
},
-1
);
}
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -0,0 +1,41 @@
<template>
<div class="statusEvent">
<i18n-t keypath="message.room_upgraded" tag="span">
<template v-slot:link>
<a v-on:click="goToSuccessor" class="clickable" href="about:blank">{{ $t("message.room_upgraded_link") }}</a>
</template>
</i18n-t>
</div>
</template>
<script setup lang="ts">
import { inject } from "vue";
import { MessageProps } from "./useMessage";
import utils from "@/plugins/utils";
const $navigation: any = inject("globalNavigation");
const props = defineProps<MessageProps>();
const goToSuccessor = (e: Event) => {
e.preventDefault();
e.stopPropagation();
const replacement_room = props.originalEvent.getContent().replacement_room;
if (replacement_room) {
$navigation.push(
{
name: "Chat",
params: {
roomId: utils.sanitizeRoomId(replacement_room),
},
},
-1
);
}
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -79,6 +79,7 @@ export default {
}; };
app.$navigation = navigationService; app.$navigation = navigationService;
app.provide("globalNavigation", navigationService);
app.config.globalProperties.$navigation = navigationService; app.config.globalProperties.$navigation = navigationService;
}, },
}; };

View file

@ -1,17 +1,17 @@
import { createStore } from 'vuex' import { createStore } from 'vuex'
import VuexPersistence from 'vuex-persist' import VuexPersistence from 'vuex-persist'
const USER = `convene_${ window.location.hostname }_user` export const STORE_KEY_USER = `convene_${ window.location.hostname }_user`
const SETTINGS = `convene_${ window.location.hostname }_settings` export const STORE_KEY_SETTINGS = `convene_${ window.location.hostname }_settings`
// A Vuex plugin to persist the user object to either session or local storage, based on flag in the store state. // A Vuex plugin to persist the user object to either session or local storage, based on flag in the store state.
// //
const persistUserPlugin = store => { const persistUserPlugin = store => {
var user; var user;
if (store.state.useLocalStorage) { if (store.state.useLocalStorage) {
user = JSON.parse(window.localStorage.getItem(USER)); user = JSON.parse(window.localStorage.getItem(STORE_KEY_USER));
} else { } else {
user = JSON.parse(window.sessionStorage.getItem(USER)); user = JSON.parse(window.sessionStorage.getItem(STORE_KEY_USER));
} }
const initialState = user ? { status: { loggedIn: true }, user } : { status: { loggedIn: false }, user: null }; const initialState = user ? { status: { loggedIn: true }, user } : { status: { loggedIn: false }, user: null };
store.state.auth = initialState; store.state.auth = initialState;
@ -19,11 +19,11 @@ const persistUserPlugin = store => {
store.subscribe((mutation, state) => { store.subscribe((mutation, state) => {
if (mutation.type == 'setUser' || mutation.type == 'setUseLocalStorage') { if (mutation.type == 'setUser' || mutation.type == 'setUseLocalStorage') {
if (state.useLocalStorage) { if (state.useLocalStorage) {
window.localStorage.setItem(USER, JSON.stringify(state.auth.user)); window.localStorage.setItem(STORE_KEY_USER, JSON.stringify(state.auth.user));
window.sessionStorage.removeItem(USER); window.sessionStorage.removeItem(STORE_KEY_USER);
} else { } else {
window.sessionStorage.setItem(USER, JSON.stringify(state.auth.user)); window.sessionStorage.setItem(STORE_KEY_USER, JSON.stringify(state.auth.user));
window.localStorage.removeItem(USER); window.localStorage.removeItem(STORE_KEY_USER);
} }
} }
}) })
@ -31,7 +31,7 @@ const persistUserPlugin = store => {
const vuexPersistLocalStorage = new VuexPersistence({ const vuexPersistLocalStorage = new VuexPersistence({
key: SETTINGS, key: STORE_KEY_SETTINGS,
storage: localStorage, storage: localStorage,
reducer: state => { reducer: state => {
if (state.useLocalStorage) { if (state.useLocalStorage) {
@ -40,6 +40,7 @@ const vuexPersistLocalStorage = new VuexPersistence({
currentRoomId: state.currentRoomId, currentRoomId: state.currentRoomId,
hasShownMissedItemsHint: state.hasShownMissedItemsHint, hasShownMissedItemsHint: state.hasShownMissedItemsHint,
globalNotification: state.globalNotification, globalNotification: state.globalNotification,
uaAccepted: state.uaAccepted,
}; };
} else { } else {
return {}; return {};
@ -48,7 +49,7 @@ const vuexPersistLocalStorage = new VuexPersistence({
}) })
const vuexPersistSessionStorage = new VuexPersistence({ const vuexPersistSessionStorage = new VuexPersistence({
key: SETTINGS, key: STORE_KEY_SETTINGS,
storage: sessionStorage, storage: sessionStorage,
reducer: state => { reducer: state => {
if (!state.useLocalStorage) { if (!state.useLocalStorage) {
@ -56,6 +57,7 @@ const vuexPersistSessionStorage = new VuexPersistence({
language: state.language, language: state.language,
currentRoomId: state.currentRoomId, currentRoomId: state.currentRoomId,
hasShownMissedItemsHint: state.hasShownMissedItemsHint, hasShownMissedItemsHint: state.hasShownMissedItemsHint,
uaAccepted: state.uaAccepted,
}; };
} else { } else {
return {}; return {};
@ -63,10 +65,10 @@ const vuexPersistSessionStorage = new VuexPersistence({
} }
}) })
const defaultUseSessionStorage = (sessionStorage.getItem(USER) != null); const defaultUseSessionStorage = (sessionStorage.getItem(STORE_KEY_USER) != null);
const store = createStore({ 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, uaAccepted: false },
mutations: { mutations: {
loginSuccess(state, user) { loginSuccess(state, user) {
state.auth.status.loggedIn = true; state.auth.status.loggedIn = true;
@ -100,6 +102,9 @@ const store = createStore({
}, },
setGlobalNotification(state, flag) { setGlobalNotification(state, flag) {
state.globalNotification = flag; state.globalNotification = flag;
},
acceptUA(state, accepted) {
state.uaAccepted = accepted;
} }
}, },
actions: { actions: {