Knock support

Also, fix token refresh functionality
This commit is contained in:
N-Pex 2025-05-28 12:29:04 +02:00
parent cfabd8be08
commit e8f04d79c9
11 changed files with 310 additions and 75 deletions

View file

@ -1178,7 +1178,11 @@ export default {
this.$navigation.push(
{
name: "Join",
params: { roomId: util.sanitizeRoomId(this.roomAliasOrId), join: this.$route.params.join },
params: {
roomId: util.sanitizeRoomId(this.roomAliasOrId),
join: this.$route.params.join
},
query: this.$route.query
},
0
);

View file

@ -90,13 +90,22 @@
<interactive-auth ref="interactiveAuth" />
<v-text-field
v-if="roomNeedsKnock"
variant="solo"
density="compact"
:label="$t('join.knock_reason')"
counter="100"
maxlength="100"
v-model="knockReason" />
<v-btn id="btn-join" class="btn-dark" :disabled="!acceptUA || (room && room.getMyMembership() == 'ban')" size="large"
@click.stop="handleJoin" :loading="loading" v-if="!currentUser">{{
roomId && roomId.startsWith("@") ? $t("join.enter_room_user") : $t("join.enter_room")
roomId && roomId.startsWith("@") ? $t("join.enter_room_user") : this.roomNeedsKnock ? $t("join.enter_knock") : $t("join.enter_room")
}}</v-btn>
<v-btn id="btn-join" class="btn-dark" :disabled="!acceptUA || (room && room.getMyMembership() == 'ban')" size="large"
block @click.stop="handleJoin" :loading="loading" v-else>{{
roomId && roomId.startsWith("@") ? $t("join.join_user") : $t("join.join")
roomId && roomId.startsWith("@") ? $t("join.join_user") : this.roomNeedsKnock ? $t("join.knock") : $t("join.join")
}}</v-btn>
<div v-if="loadingMessage" class="text-center">{{ loadingMessage }}</div>
@ -164,6 +173,20 @@ export default {
InteractiveAuth,
AuthedImage
},
props: {
roomDisplayName: {
type: String,
default: function () {
return null;
}
},
roomNeedsKnock: {
type: Boolean,
default: function () {
return false;
}
}
},
data() {
return {
roomName: null,
@ -178,6 +201,7 @@ export default {
showEditDisplaynameDialog: false,
showSelectLanguageDialog: false,
acceptUA: false,
knockReason: "",
};
},
computed: {
@ -233,14 +257,6 @@ export default {
let activeLanguages = [...this.getLanguages()];
return activeLanguages.filter((lang) => lang.value === this.$i18n.locale);
},
roomDisplayName() {
// If there is a display name in to invite link, use that!
try {
return new URL(location.href).searchParams.get('roomName');
} catch(ignoredError) {
return undefined;
}
}
},
watch: {
roomId: {
@ -312,8 +328,13 @@ export default {
this.$nextTick(() => {
this.handleJoin();
});
}
else if (this.roomId.startsWith("#")) {
} else if (this.roomId.startsWith("@")) {
// Direct chat with user
this.waitingForRoomCreation = true;
this.$nextTick(() => {
this.handleJoin();
});
} else {
this.$matrix
.getPublicRoomInfo(this.roomId)
.then((room) => {
@ -323,25 +344,18 @@ export default {
})
.catch((err) => {
console.log("Could not find room info", err);
// Private room, try to get name
const room = this.$matrix.getRoom(this.roomId);
if (room) {
this.roomName = this.removeHomeServer(room.name || this.roomName);
} else {
this.roomName = this.removeHomeServer(this.roomAliasOrId);
}
})
.finally(() => {
this.waitingForInfo = false;
});
} else if (this.roomId.startsWith("@")) {
// Direct chat with user
this.waitingForRoomCreation = true;
this.$nextTick(() => {
this.handleJoin();
});
} else {
// Private room, try to get name
const room = this.$matrix.getRoom(this.roomId);
if (room) {
this.roomName = this.removeHomeServer(room.name || this.roomName);
} else {
this.roomName = this.removeHomeServer(this.roomAliasOrId);
}
this.waitingForInfo = false;
}
},
@ -359,10 +373,6 @@ export default {
}
},
handleOpenApp() {
console.log("Open app..."); //TODO
},
handleJoin() {
this.loading = true;
this.loadingMessage = this.$t("join.status_logging_in");
@ -412,6 +422,11 @@ export default {
this.$matrix.setCurrentRoomId(room.roomId);
return room;
});
} else if (this.roomNeedsKnock) {
console.log("Join: knocking room");
this.$analytics.event("Invitations", "Room Knocked");
this.loadingMessage = this.$t("join.status_knocking");
return this.$matrix.matrixClient.knockRoom(this.roomId, this.knockReason.length > 0 ? { reason: this.knockReason} : undefined);
} else {
console.log("Join: joining room");
this.$analytics.event("Invitations", "Room Joined");
@ -423,13 +438,23 @@ export default {
this.loading = false;
this.loadingMessage = null;
this.$nextTick(() => {
this.$navigation.push(
{
name: "Chat",
params: { roomId: util.sanitizeRoomId(room.roomId) },
},
-1
);
if (this.roomNeedsKnock) {
// For knocks, send to room list
this.$navigation.push(
{
name: "Home",
},
-1
);
} else {
this.$navigation.push(
{
name: "Chat",
params: { roomId: util.sanitizeRoomId(room.roomId) },
},
-1
);
}
});
})
.catch((err) => {

View file

@ -192,7 +192,7 @@
<v-list-item
class="member"
v-show="showAllMembers || index < SHOW_MEMBER_LIMIT"
@click="onListItemClick(member)"
@click="onMemberClick(member)"
>
<div>
<div class="user-icon-with-badge">
@ -236,6 +236,81 @@
</div>
</v-card>
<!-- KNOCKS, only shown is joinRule is knock -->
<v-card class="members ma-3" variant="flat" v-if="iAmAdmin && roomJoinRule == 'knock'">
<v-card-title class="h2"
>{{ $t("room_info.knocks") }}<v-spacer></v-spacer>
<div>{{ knocks.length }}</div></v-card-title
>
<v-list>
<template v-for="(member, index) in knocks" :key="member.userId">
<v-list-item
class="member"
v-show="showAllKnocks || index < SHOW_MEMBER_LIMIT"
@click="onKnockClick(member)"
>
<div>
<div class="user-icon-with-badge">
<v-avatar class="avatar" size="32" color="grey">
<AuthedImage v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="text-white headline">{{
member.name.substring(0, 1).toUpperCase()
}}</span>
</v-avatar>
<v-avatar circle class="user-badge" size="15" v-if="isAdmin(member) || isModerator(member)">
<v-icon style="min-width: 15px;min-height: 15px;">{{ `$vuetify.icons.${isAdmin(member)? 'make_admin' : 'make_moderator'}` }}</v-icon>
</v-avatar>
</div>
<span class="user-name">
{{
member.userId == $matrix.currentUserId
? $t("room_info.user_you", {
user: member.user ? member.user.displayName : member.name,
})
: $t("room_info.user", {
user: member.user ? member.user.displayName : member.name,
})
}}
</span>
<span v-if="isAdmin(member)" class="user-power">
{{ $t("room_info.user_admin") }}
</span>
<span v-else-if="isModerator(member)" class="user-power">
{{ $t("room_info.user_moderator") }}
</span>
<div class="knock-reason">{{ member.events?.member ? member.events?.member.getContent().reason : "" }}</div>
</div>
<!-- Accept/Deny knock buttons-->
<template v-slot:append>
<v-list-item-action>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-btn color="transparent" v-bind="props" @click.stop="acceptKnock(member)" icon="thumb_up"></v-btn>
</template>
<span>{{ $t("room_info.accept_knock") }}</span>
</v-tooltip>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-btn color="transparent" v-bind="props" @click.stop="rejectKnock(member)" icon="block"></v-btn>
</template>
<span>{{ $t("room_info.reject_knock") }}</span>
</v-tooltip>
</v-list-item-action>
</template>
</v-list-item>
<v-divider
v-if="(showAllKnocks || index < SHOW_MEMBER_LIMIT) && index < knocks.length - 1"
:key="index"
></v-divider>
</template>
</v-list>
<div class="show-all p-2" @click="showAllKnocks = !showAllKnocks" v-if="knocks.length > SHOW_MEMBER_LIMIT">
{{ showAllKnocks ? $t("room_info.hide_all") : $t("room_info.show_all") }}
</div>
</v-card>
<!-- EXPORT CHAT -->
<div style="text-align: center">
<v-btn
@ -314,8 +389,10 @@ export default {
data() {
return {
members: [],
knocks: [],
user: null,
showAllMembers: false,
showAllKnocks: false,
showMemberActionConfirmation: false,
showLeaveConfirmation: false,
showPurgeConfirmation: false,
@ -334,6 +411,11 @@ export default {
text: this.$t("room_info.join_invite"),
icon: "person_add",
},
{
id: "knock",
text: this.$t("room_info.join_knock"),
icon: "doorbell",
},
],
SHOW_MEMBER_LIMIT: 5,
exporting: false,
@ -430,10 +512,21 @@ export default {
const retentionPeriodsFound = this.retentionPeriods.find(rp => rp.value===retention)
this.messageRetentionDisplay = retentionPeriodsFound.text
},
onListItemClick(member) {
onMemberClick(member) {
this.activeMember = member
this.showMemberActionConfirmation = true
},
onKnockClick(member) {
this.activeMember = member
this.showMemberActionConfirmation = true
},
acceptKnock(member) {
this.$matrix.answerKnock(member.roomId, member.userId, true, undefined);
},
rejectKnock(member) {
this.$matrix.answerKnock(member.roomId, member.userId, false, undefined);
},
onEvent(event) {
if (this.room && this.room.roomId == event.getRoomId()) {
// For this room
@ -466,8 +559,30 @@ export default {
const bName = b.user ? b.user.displayName : b.name;
return aName.localeCompare(bName);
});
this.knocks = this.room.getMembersWithMembership("knock").sort((a, b) => {
// Place ourselves at the top!
if (a.userId == myUserId) {
return -1;
} else if (b.userId == myUserId) {
return 1;
}
// Then sort by power level
if (a.powerLevel > b.powerLevel) {
return -1;
} else if (b.powerLevel > a.powerLevel) {
return 1;
}
// Then by name
const aName = a.user ? a.user.displayName : a.name;
const bName = b.user ? b.user.displayName : b.name;
return aName.localeCompare(bName);
});
} else {
this.members = [];
this.knocks = [];
}
},

View file

@ -37,6 +37,29 @@
</template>
</v-list-item>
<!-- Knocked rooms -->
<v-list-item :disabled="roomsProcessing[room.roomId]" v-for="room in knockedRooms" :key="room.roomId"
:value="room.roomId" class="room-list-room">
<template v-slot:prepend>
<v-avatar size="42" color="#d9d9d9">
<AuthedImage v-if="roomAvatar(room)" :src="roomAvatar(room)" />
<span v-else class="text-white headline">{{
room.name.substring(0, 1).toUpperCase()
}}</span>
</v-avatar>
</template>
<v-list-item-title class="room-list-name">{{ room.name }}</v-list-item-title>
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
<template v-slot:append>
<v-list-item-action>
<v-btn id="btn-reject" class="filled-button" color="black"
@click.stop="rejectInvitation(room)" variant="text">{{
$t("menu.cancel_knock") }}</v-btn>
</v-list-item-action>
</template>
</v-list-item>
<v-list-item v-for="room in joinedRooms" :key="room.roomId" :value="room.roomId"
@click="currentRoomId = room.roomId" class="room-list-room">
<template v-slot:prepend>
@ -96,6 +119,9 @@ export default {
invitedRooms() {
return this.sortItemsOnName(this.$matrix.invites);
},
knockedRooms() {
return this.sortItemsOnName(this.$matrix.knockedRooms);
},
joinedRooms() {
// show room with notification on top, followed by room decending order by active Timestamp
return [...this.$matrix.joinedRooms].sort((a, b) => {

View file

@ -37,6 +37,9 @@
</div>
<DeviceList :member="activeMember" />
<div class="py-3" v-if="activeMember.userId != $matrix.currentUserId">
<div v-if="activeMember.membership == 'knock'">
</div>
<div v-else>
<v-btn variant="text" size="x-large" block v-if="activeMember.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, activeMember.userId)" class="start-private-chat clickable d-block text-none justify-start" @click="startPrivateChat(activeMember.userId)">
<v-icon start>$vuetify.icons.direct_chat</v-icon> {{ $t("menu.direct_chat") }}
</v-btn>
@ -54,6 +57,7 @@
<v-btn variant="text" size="x-large" block v-if="activeMember.userId != $matrix.currentUserId && isModeratorComp && canRevokeModeratorComp" class="start-private-chat clickable d-block text-none justify-start" @click="revokeModerator(activeMember)">
<v-icon start>$vuetify.icons.revoke</v-icon> {{ $t("menu.user_revoke_moderator") }}
</v-btn>
</div>
</div>
</div>
</v-dialog>

View file

@ -162,10 +162,12 @@ export default {
return ContactJoin;
}
} else if (event.getContent().membership == "leave") {
if ((event.getPrevContent() || {}).membership == "join" &&
event.getStateKey() != event.getSender()) {
return ContactKicked;
}
if ((event.getPrevContent() || {}).membership == "join" && event.getStateKey() != event.getSender()) {
return ContactKicked;
}
if ((event.getPrevContent() || {}).membership == "knock") {
return null; // A knock that was rejected
}
return ContactLeave;
} else if (this.showAllStatusMessages) {
if (event.getContent().membership == "invite") {

View file

@ -62,13 +62,14 @@ export default {
},
publicRoomLink() {
if (this.room && this.roomJoinRule == "public") {
if (this.room && (this.roomJoinRule == "public" || this.roomJoinRule == "knock")) {
return this.$router.getRoomLink(
this.room.getCanonicalAlias(),
this.room.roomId,
this.room.name,
utils.roomDisplayTypeToQueryParam(this.room, this.roomDisplayType),
this.roomDisplayType == ROOM_TYPE_CHANNEL /* Auto join for channels */
this.roomDisplayType == ROOM_TYPE_CHANNEL, /* Auto join for channels */
this.roomJoinRule == "knock"
);
}
return null;