Restore matrix impl and remove $tc

This commit is contained in:
N-Pex 2025-05-06 11:28:47 +02:00
parent c47b975723
commit 7a801f3ec3
8 changed files with 1140 additions and 696 deletions

View file

@ -28,7 +28,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="num-members">{{ $tc("room.members", memberCount) }}</div> <div class="num-members">{{ $t("room.members", memberCount) }}</div>
</v-col> </v-col>
<v-col cols="auto" class="text-end ma-0 pa-0 ms-1"> <v-col cols="auto" class="text-end ma-0 pa-0 ms-1">
<v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26" <v-avatar :class="{ 'avatar-32': true, 'clickable': true, 'popup-open': showProfileInfo }" size="26"
@ -117,11 +117,11 @@ export default {
notificationsText() { notificationsText() {
const invitationCount = this.$matrix.invites.length const invitationCount = this.$matrix.invites.length
if (invitationCount > 0) { if (invitationCount > 0) {
return this.$tc('room.invitations', invitationCount); return this.$t('room.invitations', invitationCount);
} }
const missedMessagesCount = this.$matrix.joinedRooms.reduce((value, r) => ((r.roomId !== this.$matrix.currentRoomId && r.getCanonicalAlias() !== this.$matrix.currentRoomId) ? (value + r.getUnreadNotificationCount("total")) : value), 0); const missedMessagesCount = this.$matrix.joinedRooms.reduce((value, r) => ((r.roomId !== this.$matrix.currentRoomId && r.getCanonicalAlias() !== this.$matrix.currentRoomId) ? (value + r.getUnreadNotificationCount("total")) : value), 0);
if (missedMessagesCount > 0) { if (missedMessagesCount > 0) {
return this.$tc('room.unseen_messages', missedMessagesCount); return this.$t('room.unseen_messages', missedMessagesCount);
} }
return ""; return "";
}, },

View file

@ -8,7 +8,7 @@
<div class="room-name-inline text-truncate" :title="room.name"> <div class="room-name-inline text-truncate" :title="room.name">
{{ room.name }} {{ room.name }}
</div> </div>
<div class="num-members">{{ $tc("room.members", room.getJoinedMemberCount()) }}</div> <div class="num-members">{{ $t("room.members", room.getJoinedMemberCount()) }}</div>
</v-col> </v-col>
<v-col cols="auto" class="text-end ma-0 pa-0">{{ exportDate }}</v-col> <v-col cols="auto" class="text-end ma-0 pa-0">{{ exportDate }}</v-col>

View file

@ -119,7 +119,7 @@ export default {
dayForEvent(event) { dayForEvent(event) {
let dayDiff = util.dayDiffToday(event.getTs()); let dayDiff = util.dayDiffToday(event.getTs());
if (dayDiff < 7) { if (dayDiff < 7) {
return this.$tc("message.time_ago", dayDiff); return this.$t("message.time_ago", dayDiff);
} else { } else {
return util.formatDay(event.getTs()); return util.formatDay(event.getTs());
} }

View file

@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container"> <div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
<div class="file-drop-files-sent">{{ $tc((this.messageInput && this.messageInput.length > 0) ? <div class="file-drop-files-sent">{{ $t((this.messageInput && this.messageInput.length > 0) ?
"file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}</div> "file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}</div>
<div class="file-drop-section"> <div class="file-drop-section">
<v-textarea disabled full-width solo flat auto-grow v-model="messageInput" no-resize class="input-area-text" <v-textarea disabled full-width solo flat auto-grow v-model="messageInput" no-resize class="input-area-text"

View file

@ -16,7 +16,7 @@
</transition-group> </transition-group>
</div> </div>
</template> </template>
<span>{{ $tc("message.seen_by_count", seenBy.length) }}</span> <span>{{ $t("message.seen_by_count", seenBy.length) }}</span>
</v-tooltip> </v-tooltip>
</div> </div>
<BottomSheet <BottomSheet
@ -24,7 +24,7 @@
ref="seenByListBottomSheet" ref="seenByListBottomSheet"
> >
<v-list> <v-list>
<v-subheader class="text-uppercase"> {{ $tc("message.seen_by") }}</v-subheader> <v-subheader class="text-uppercase"> {{ $t("message.seen_by") }}</v-subheader>
<v-list-item v-for="(member, index) in seenBy" :key="index" class="text-left"> <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">
@ -96,7 +96,7 @@ export default {
seenByTimeStamp(timestamp) { seenByTimeStamp(timestamp) {
let dayDiff = utils.dayDiffToday(timestamp); let dayDiff = utils.dayDiffToday(timestamp);
if (dayDiff < 3) { if (dayDiff < 3) {
return this.$tc("message.time_ago", dayDiff) + ' '+utils.formatTime(timestamp); return this.$t("message.time_ago", dayDiff) + ' '+utils.formatTime(timestamp);
} else { } else {
return utils.formatTime(timestamp); return utils.formatTime(timestamp);
} }

View file

@ -306,11 +306,11 @@ export default {
if (ti < 60) { if (ti < 60) {
s = this.$t("global.time.recently"); s = this.$t("global.time.recently");
} else if (ti < 3600 && Math.round(ti / 60) < 60) { } else if (ti < 3600 && Math.round(ti / 60) < 60) {
s = this.$tc("global.time.minutes", Math.round(ti / 60)); s = this.$t("global.time.minutes", Math.round(ti / 60));
} else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) { } else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) {
s = this.$tc("global.time.hours", Math.round(ti / 60 / 60)); s = this.$t("global.time.hours", Math.round(ti / 60 / 60));
} else { } else {
s = this.$tc("global.time.days", Math.round(ti / 60 / 60 / 24)); s = this.$t("global.time.days", Math.round(ti / 60 / 60 / 24));
} }
return this.toLocalNumbers(s); return this.toLocalNumbers(s);
}, },

View file

@ -182,6 +182,8 @@ app.use(i18n);
app.use(matrix, { store: store, i18n: i18n }); app.use(matrix, { store: store, i18n: i18n });
app.config.globalProperties.$root = app;
//app.use(matrix); //app.use(matrix);
//app.use(config); //app.use(config);
// app.use(analytics); // app.use(analytics);

View file

@ -1,11 +1,11 @@
import { reactive, createApp } from "vue"; import { 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 } from "matrix-js-sdk"; import { TimelineWindow, EventTimeline, EventStatus } from "matrix-js-sdk";
import util from "../plugins/utils"; import util, { STATE_EVENT_ROOM_DELETED, STATE_EVENT_ROOM_TYPE, ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE, ROOM_TYPE_DEFAULT } from "../plugins/utils";
import User from "../models/user"; import User from "../models/user";
import { LocalStorageCryptoStore } from "matrix-js-sdk/lib/crypto/store/localStorage-crypto-store"; import * as LocalStorageCryptoStoreClass 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
@ -72,8 +72,8 @@ export default {
return null; return null;
}, },
currentUserHomeServer() { currentUserMXDomain() {
return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId); return User.domainPart(this.currentUserId) || this.$config.defaultMatrixDomainPart;
}, },
currentRoomId() { currentRoomId() {
@ -91,6 +91,12 @@ 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: {
@ -100,6 +106,18 @@ 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: {
@ -107,9 +125,11 @@ 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) { login(user, registrationFlowHandler, createUser = false) {
return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
const tempMatrixClient = sdk.createClient({ const tempMatrixClient = sdk.createClient({
baseUrl: user.home_server, baseUrl: baseUrl,
idBaseUrl: this.$config.identityServer,
}); });
var promiseLogin; var promiseLogin;
@ -117,27 +137,52 @@ 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 (user.is_guest && !user.user_id) { } else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) {
// 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 user = util.randomUser(this.$config.userIdPrefix); const userId =
const pass = util.randomPass(); createUser || user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix);
promiseLogin = tempMatrixClient const pass = createUser || user.registration_session ? user.password : util.randomPass();
.register(user, pass, null, {
type: "m.login.dummy", const extractAndSaveUser = (response) => {
initial_device_display_name: this.$config.appName,
})
.then((response) => {
console.log("Response", response);
var u = Object.assign({}, response); var u = Object.assign({}, response);
u.home_server = tempMatrixClient.baseUrl; // Don't use deprecated field from response.
u.password = pass; u.password = pass;
u.is_guest = true; u.is_guest = true;
this.$store.commit("setUser", u); this.$store.commit("setUser", u);
return u; return u;
};
promiseLogin = tempMatrixClient
.register(userId, pass, user.registration_session || 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;
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;
}); });
} else { } else {
var data = { var data = {
@ -155,7 +200,6 @@ 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;
}); });
@ -164,6 +208,7 @@ export default {
return promiseLogin.then((user) => { return promiseLogin.then((user) => {
return self.getMatrixClient(user); return self.getMatrixClient(user);
}); });
});
}, },
clearCryptoStore() { clearCryptoStore() {
@ -237,24 +282,25 @@ 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: user.home_server, baseUrl: baseUrl,
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.log("MATRIX CLIENT", this.matrixClient); console.error("Created client", this.matrixClient);
return this.matrixClient return this.matrixClient
.initCrypto() .initRustCrypto()
.then(() => { .then(() => {
console.log("Crypto initialized"); console.log("Crypto initialized");
@ -281,10 +327,16 @@ 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;
}); });
});
}, },
/** /**
@ -293,11 +345,14 @@ 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() { getLoginPromise(registrationFlowHandler) {
if (this.ready) { if (this.ready) {
return Promise.resolve(this.currentUser); return Promise.resolve(this.currentUser);
} }
return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true)); return this.$store.dispatch("login", {
user: this.currentUser || new User("", "", true),
registrationFlowHandler,
});
}, },
addMatrixClientListeners(client) { addMatrixClientListeners(client) {
@ -325,7 +380,7 @@ export default {
{ {
const room = this.matrixClient.getRoom(event.getRoomId()); const room = this.matrixClient.getRoom(event.getRoomId());
if (room) { if (room) {
room.topic = event.getContent().topic; room["topic"] = event.getContent().topic;
} }
} }
break; break;
@ -334,24 +389,56 @@ export default {
{ {
const room = this.matrixClient.getRoom(event.getRoomId()); const room = this.matrixClient.getRoom(event.getRoomId());
if (room) { if (room) {
room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true); Vue.set(
room,
"avatar",
room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia)
);
} }
} }
break; break;
case "m.room.member": case "m.room.power_levels":
{ {
if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) { if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
// Don't use this.currentRoomId, may be an alias. We need the real id! this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(event.getRoomId(), this.currentUserId);
if ( this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(event.getRoomId(), this.currentUserId);
event.getContent().membership == "leave" && }
(event.getPrevContent() || {}).membership == "join" && }
event.getStateKey() == this.currentUserId && break;
event.getSender() != this.currentUserId
) { case "m.room.join_rules":
// We were kicked {
const wasPurged = event.getContent().reason == "Room Deleted"; const room = this.matrixClient.getRoom(event.getRoomId());
this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1); 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);
});
}
} }
} }
} }
@ -360,8 +447,19 @@ export default {
this.updateNotificationCount(); this.updateNotificationCount();
}, },
onRoom(ignoredroom) { onRoom(room) {
console.log("Got room", ignoredroom); if (room.selfMembership === "invite") {
this.matrixClient
.getRoomTags(room.roomId)
.then((reply) => {
if (Object.keys(reply.tags).includes("m.server_notice")) {
room["isServiceNoticeRoom"] = true;
}
})
.catch((error) => {
console.error(error);
});
}
this.reloadRooms(); this.reloadRooms();
this.updateNotificationCount(); this.updateNotificationCount();
}, },
@ -416,19 +514,25 @@ 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"); return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join") && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0;
}); });
updatedRooms.forEach((room) => { updatedRooms.forEach((room) => {
if (!room.avatar) { room["avatar"] = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia);
room.avatar = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true);
}
}); });
console.log("Reload rooms", updatedRooms); this["rooms"] = updatedRooms;
this.rooms = updatedRooms;
const currentRoom = this.getRoom(this.$store.state.currentRoomId); 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);
if (this.currentRoom != currentRoom) { if (this.currentRoom != currentRoom) {
this.currentRoom = currentRoom; this.currentRoom = currentRoom;
} }
})
.catch((ignorederror) => { });
}, },
setCurrentRoomId(roomId) { setCurrentRoomId(roomId) {
@ -494,15 +598,178 @@ export default {
}, },
leaveRoom(roomId) { leaveRoom(roomId) {
return this.matrixClient.leave(roomId, undefined).then(() => { return this.matrixClient.leave(roomId).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
@ -513,6 +780,30 @@ 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);
@ -521,22 +812,39 @@ 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"));
this.matrixClient withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "private" }, ""))
.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "invite" }, "")
.then(() => { .then(() => {
//console.log("Purge: forbid guest access"); //console.log("Purge: forbid guest access");
return this.matrixClient.sendStateEvent(roomId, "m.room.guest_access", { guest_access: "forbidden" }, ""); return withRetry(() => this.matrixClient.sendStateEvent(
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 this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", { return withRetry(() => 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");
@ -546,8 +854,11 @@ 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((ignoredsuccess) => { return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((gotmore) => {
if (gotmore) {
return _getMoreIfAvailable.call(self); return _getMoreIfAvailable.call(self);
}
return Promise.resolve("Done");
}); });
} else { } else {
return Promise.resolve("Done"); return Promise.resolve("Done");
@ -560,40 +871,92 @@ 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);
var redactionPromises = []; const allEvents = timelineWindow.getEvents().filter((event) => {
timelineWindow.getEvents().forEach((event) => { return (
if (!event.isRedacted() && !event.isRedaction() && !event.isState()) { !event.isRedacted() &&
// Redact! !event.isRedaction() &&
redactionPromises.push(this.matrixClient.redactEvent(event.getRoomId(), event.getId())); !event.isState() &&
} (!room.currentState || room.currentState.maySendRedactionForEvent(event, this.currentUserId))
);
}); });
return Promise.all(redactionPromises);
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(() =>
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 members = joined.concat(invited); var allMembers = joined.concat(invited);
var kickPromises = []; const me = allMembers.find((m) => m.userId == self.currentUserId);
members.forEach((member) => {
if (member.userId != self.currentUserId) { const kickFirstMember = (members) => {
kickPromises.push(this.matrixClient.kick(roomId, member.userId, "Room Deleted")); //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);
} }
}); const member = members[0];
return Promise.all(kickPromises); 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" })
);
}) })
.then(() => { .then(() => {
statusCallback(null); statusCallback(null);
this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting); this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
return this.leaveRoom(roomId); return withRetry(() => 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);
}); });
@ -649,7 +1012,17 @@ export default {
type: "m.room.history_visibility", type: "m.room.history_visibility",
state_key: "", state_key: "",
content: { content: {
history_visibility: "joined", history_visibility: "invited",
},
},
{
type: "m.room.power_levels",
state_key: "",
content: {
users: {
[this.currentUserId]: 100,
[userId]: 100,
},
}, },
}, },
], ],
@ -672,7 +1045,7 @@ export default {
* @param {*} userId * @param {*} userId
*/ */
isDirectRoomWith(room, userId) { isDirectRoomWith(room, userId) {
if (room.getJoinRule() == "invite" && room.getMembers().length == 2) { if (room && 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") {
@ -687,6 +1060,20 @@ 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);
@ -699,6 +1086,17 @@ 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 = {
@ -732,31 +1130,26 @@ export default {
return this.matrixClient.uploadContent(file, opts); return this.matrixClient.uploadContent(file, opts);
}, },
getPublicRoomInfo(roomId) { /**
if (!roomId) { * Get a matrix client that can be used for public queries. If we are logged in, this is the normal
return Promise.reject("Invalid parameters"); * matrix client. If not, we create a temp one with a temp password.
} * @returns A MatrixClient that can be used for public queries
*/
const parts = roomId.split(":"); getPublicQueryMatrixClient() {
if (parts.length != 2) {
return Promise.reject("Unknown room server");
}
const server = parts[1];
var clientPromise;
if (this.matrixClient) { if (this.matrixClient) {
clientPromise = this.getMatrixClient().then(() => { return 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?
// //
@ -799,7 +1192,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: this.$config.defaultServer, baseUrl: baseUrl,
userId: user.user_id, userId: user.user_id,
accessToken: user.access_token, accessToken: user.access_token,
timelineSupport: false, timelineSupport: false,
@ -808,16 +1201,33 @@ export default {
matrixClient.startClient(); matrixClient.startClient();
return matrixClient; return matrixClient;
}); });
return clientPromise;
});
}
},
getPublicRoomInfo(roomId) {
if (!roomId) {
return Promise.reject("Invalid parameters");
} }
const findOrGetMore = function _findOrGetMore(client, response) { 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) {
for (var room of response.chunk) { for (var room of response.chunk) {
if ( if (
(roomId.startsWith("#") && room.canonical_alias == roomId) || (roomId.startsWith("#") && room.canonical_alias == roomId) ||
(roomId.startsWith("!") && room.room_id == roomId) (roomId.startsWith("!") && room.room_id == roomId)
) { ) {
if (room.avatar_url) { if (room.avatar_url) {
room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", true); room["avatar"] = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", false, undefined, useAuthedMedia);
} }
return Promise.resolve(room); return Promise.resolve(room);
} }
@ -841,13 +1251,18 @@ export default {
}; };
var matrixClient; var matrixClient;
let useAuthedMedia = false;
return clientPromise return clientPromise
.then((client) => { .then((client) => {
matrixClient = client; matrixClient = client;
return client.isVersionSupported("v1.11");
})
.then((version1_11) => {
useAuthedMedia = version1_11;
return matrixClient.publicRooms({ server: server, limit: 1000 }); return matrixClient.publicRooms({ server: server, limit: 1000 });
}) })
.then((response) => { .then((response) => {
return findOrGetMore(matrixClient, response); return findOrGetMore(matrixClient, useAuthedMedia, response);
}) })
.catch((err) => { .catch((err) => {
return Promise.reject("Failed to find room: " + err); return Promise.reject("Failed to find room: " + err);
@ -861,6 +1276,33 @@ 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: () => {} render: () => {}
}); });