diff --git a/src/assets/css/chat.scss b/src/assets/css/chat.scss
index 1a2c77e..c69b606 100644
--- a/src/assets/css/chat.scss
+++ b/src/assets/css/chat.scss
@@ -1015,6 +1015,12 @@ body {
margin-left: 6px;
}
+ .member .knock-reason {
+ margin-left: 38px;
+ font-size: 0.7em;
+ color: #aaa;
+ }
+
.member .user-power {
margin-left: 6px;
color: #aaa;
diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json
index 12feb55..f922d0b 100644
--- a/src/assets/translations/en.json
+++ b/src/assets/translations/en.json
@@ -48,7 +48,8 @@
"user_make_moderator": "Make moderator",
"user_revoke_moderator": "Revoke moderator",
"pin": "Pin post",
- "unpin": "Unpin post"
+ "unpin": "Unpin post",
+ "cancel_knock": "Cancel knock"
},
"message": {
"you": "You",
@@ -138,6 +139,7 @@
"room_history_joined": "People can only see the messages sent after they join.",
"join_public": "Anyone can join by opening this link: {link}.",
"join_invite": "Only people you invite can join.",
+ "join_knock": "People can request to join by ´knocking´.",
"info_permissions": "You can change ‘join permissions’ at any time in the room settings.",
"got_it": "Got it",
"no_past_messages": "Welcome! For your security, past messages are not available.",
@@ -277,10 +279,14 @@
"joining_as": "You are joining as:",
"join": "Join room",
"join_user": "Start chat",
+ "knock": "Knock",
+ "knock_reason": "Knock reason (optional)",
"enter_room": "Enter room",
"enter_room_user": "Start chat",
+ "enter_knock": "Request to join",
"status_logging_in": "Logging in...",
"status_joining": "Joining room...",
+ "status_knocking": "Knocking...",
"join_failed": "Failed to join room.",
"choose_name": "Choose a name to use",
"you_have_been_banned": "You have been banned from this room.",
@@ -328,11 +334,15 @@
"permissions": "Join Permissions",
"join_invite": "Only People Added",
"join_public": "Anyone with a link",
+ "join_knock": "People can ´knock´.",
"copy_invite_link": "Copy invite link",
"copy_link": "Copy link",
"link_copied": "Link copied!",
"purge": "Delete room",
"members": "Members",
+ "knocks": "Knocks",
+ "accept_knock": "Accept",
+ "reject_knock": "Deny",
"user": "{user}",
"user_you": "{user} (you)",
"hide_all": "Hide",
diff --git a/src/components/Chat.vue b/src/components/Chat.vue
index f679653..b7289d6 100644
--- a/src/components/Chat.vue
+++ b/src/components/Chat.vue
@@ -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
);
diff --git a/src/components/Join.vue b/src/components/Join.vue
index 62c4055..b8ed847 100644
--- a/src/components/Join.vue
+++ b/src/components/Join.vue
@@ -90,13 +90,22 @@
+
+
{{
- 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")
}}
{{
- 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")
}}
{{ loadingMessage }}
@@ -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) => {
diff --git a/src/components/RoomInfo.vue b/src/components/RoomInfo.vue
index a2c868a..183b2e7 100644
--- a/src/components/RoomInfo.vue
+++ b/src/components/RoomInfo.vue
@@ -192,7 +192,7 @@
@@ -236,6 +236,81 @@
+
+
+ {{ $t("room_info.knocks") }}
+ {{ knocks.length }}
+
+
+
+
+
+
+
+ {{
+ member.name.substring(0, 1).toUpperCase()
+ }}
+
+
+ {{ `$vuetify.icons.${isAdmin(member)? 'make_admin' : 'make_moderator'}` }}
+
+
+
+ {{
+ 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,
+ })
+ }}
+
+
+ {{ $t("room_info.user_admin") }}
+
+
+ {{ $t("room_info.user_moderator") }}
+
+
{{ member.events?.member ? member.events?.member.getContent().reason : "" }}
+
+
+
+
+
+
+
+
+
+ {{ $t("room_info.accept_knock") }}
+
+
+
+
+
+ {{ $t("room_info.reject_knock") }}
+
+
+
+
+
+
+
+
+
+ {{ showAllKnocks ? $t("room_info.hide_all") : $t("room_info.show_all") }}
+
+
+
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 = [];
}
},
diff --git a/src/components/RoomList.vue b/src/components/RoomList.vue
index 0bdaee6..13a0a97 100644
--- a/src/components/RoomList.vue
+++ b/src/components/RoomList.vue
@@ -37,6 +37,29 @@
+
+
+
+
+
+ {{
+ room.name.substring(0, 1).toUpperCase()
+ }}
+
+
+
+ {{ room.name }}
+ {{ room.topic }}
+
+
+ {{
+ $t("menu.cancel_knock") }}
+
+
+
+
@@ -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) => {
diff --git a/src/components/UserProfileDialog.vue b/src/components/UserProfileDialog.vue
index f5525bb..f027313 100644
--- a/src/components/UserProfileDialog.vue
+++ b/src/components/UserProfileDialog.vue
@@ -37,6 +37,9 @@
+
+
+
$vuetify.icons.direct_chat {{ $t("menu.direct_chat") }}
@@ -54,6 +57,7 @@
$vuetify.icons.revoke {{ $t("menu.user_revoke_moderator") }}
+
diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js
index 45aa163..4d9be43 100644
--- a/src/components/chatMixin.js
+++ b/src/components/chatMixin.js
@@ -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") {
diff --git a/src/components/roomInfoMixin.js b/src/components/roomInfoMixin.js
index 04dc632..8d2b949 100644
--- a/src/components/roomInfoMixin.js
+++ b/src/components/roomInfoMixin.js
@@ -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;
diff --git a/src/router/index.js b/src/router/index.js
index aef0b16..be8794a 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -31,7 +31,7 @@ const routes = [
{
path: '/info',
name: 'RoomInfo',
- component: defineAsyncComponent(() => import('../components/RoomInfo.vue')),
+ component: () => import('../components/RoomInfo.vue'),
props: true,
meta: {
title: 'Info',
@@ -85,7 +85,13 @@ const routes = [
{
path: '/join/:join(join/)?:roomId?',
name: 'Join',
- component: Join
+ component: Join,
+ props: route => {
+ return {
+ roomNeedsKnock: (route.query.knock ? true : false),
+ roomDisplayName: route.query.roomName
+ }
+ }
},
{
path: '/user/:userId?',
@@ -202,7 +208,7 @@ router.beforeEach((to, from, next) => {
}
});
-router.getRoomLink = function (alias, roomId, roomName, mode, autojoin) {
+router.getRoomLink = function (alias, roomId, roomName, mode, autojoin, knock) {
let params = {};
if ((!alias || roomName.replace(/\s/g, "").toLowerCase() !== util.getRoomNameFromAlias(alias)) && roomName) {
// There is no longer a correlation between alias and room name, probably because room name has
@@ -213,14 +219,17 @@ router.getRoomLink = function (alias, roomId, roomName, mode, autojoin) {
// Optional mode given, append as "m" query param
params["m"] = mode;
}
+ if (knock) {
+ params["knock"] = "1";
+ }
const autoJoinSegment = autojoin ? "join/" : "";
+ let queryString = "";
if (Object.entries(params).length > 0) {
- const queryString = Object.entries(params)
+ queryString = "?" + Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
- return window.location.origin + window.location.pathname + "?" + queryString + "#/room/" + autoJoinSegment + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
}
- return window.location.origin + window.location.pathname + "#/room/" + autoJoinSegment + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
+ return window.location.origin + window.location.pathname + "#/room/" + autoJoinSegment + encodeURIComponent(util.sanitizeRoomId(alias || roomId)) + queryString;
}
router.getDMLink = function (user, config) {
diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js
index 7870a8b..7a85f70 100644
--- a/src/services/matrix.service.js
+++ b/src/services/matrix.service.js
@@ -46,6 +46,7 @@ export default {
currentRoom: null,
notificationCount: 0,
legacyCryptoStore: undefined,
+ tokenRefreshPromise: undefined,
};
},
@@ -95,6 +96,12 @@ export default {
});
},
+ knockedRooms() {
+ return this.rooms.filter((room) => {
+ return room.getMyMembership() === "knock";
+ });
+ },
+
joinedAndInvitedRooms() {
return this.rooms.filter((room) => {
return room.getMyMembership() === "join" || room.getMyMembership() === "invite";
@@ -196,6 +203,7 @@ export default {
password: user.password,
type: "m.login.password",
initial_device_display_name: this.$config.appName,
+ refresh_token: true
};
if (user.device_id) {
data.device_id = user.device_id;
@@ -420,6 +428,7 @@ export default {
// Reject the invite, i.e. call "leave" on it.
this.matrixClient.leave(room.roomId);
}
+ // TODO - handle knocks
}
break;
@@ -469,8 +478,12 @@ export default {
this.updateNotificationCount();
},
- onRoomMyMembership(room) {
- if (room.getMyMembership() === "invite") {
+ onRoomMyMembership(room, membership, prevMembership) {
+ if (membership === "invite") {
+ if (prevMembership === "knock") {
+ // This is someone accepting our knock! Auto-join.
+ this.matrixClient.joinRoom(room.roomId);
+ }
// Invitation. Need to call "recalculate" to pick
// up room name, not sure why exactly.
room.recalculate();
@@ -479,25 +492,36 @@ export default {
},
onSessionRefresh(refreshToken) {
+ if (this.tokenRefreshPromise) {
+ return this.tokenRefreshPromise;
+ }
const now = Date.now();
- return this.matrixClient.refreshToken(refreshToken).then((result) => {
- // Store new one!
- var user = this.$store.state.auth.user;
- user.access_token = result.access_token;
- user.refresh_token = result.refresh_token;
- user.expires_in_ms = result.expires_in_ms;
- this.$store.commit("setUser", user);
+ this.tokenRefreshPromise =
+ this.matrixClient.http.request(sdk.Method.Post, "/refresh", undefined, { refresh_token: refreshToken}, { prefix: sdk.ClientPrefix.V3, inhibitLogoutEmit: true }).then((result) => {
+ // Store new one!
+ var user = this.$store.state.auth.user;
+ user.access_token = result.access_token;
+ user.refresh_token = result.refresh_token;
+ user.expires_in_ms = result.expires_in_ms;
+ this.$store.commit("setUser", user);
- // Return AccessTokens struct
- let accesssTokens = {
- accessToken: result.access_token,
- refreshToken: result.refresh_token
- };
- if (result.expires_in_ms) {
- accesssTokens.expiry = new Date(now + result.expires_in_ms);
- };
- return accesssTokens;
- });
+ // Return AccessTokens struct
+ let accessTokens = {
+ accessToken: result.access_token,
+ refreshToken: result.refresh_token
+ };
+ if (result.expires_in_ms) {
+ accessTokens.expiry = new Date(now + result.expires_in_ms);
+ };
+ return accessTokens;
+ })
+ .catch((error) => {
+ throw new sdk.TokenRefreshLogoutError(error);
+ })
+ .finally(() => {
+ this.tokenRefreshPromise = undefined;
+ });
+ return this.tokenRefreshPromise;
},
onSessionLoggedOut() {
@@ -541,7 +565,7 @@ export default {
// each time!
var updatedRooms = this.matrixClient.getVisibleRooms();
updatedRooms = updatedRooms.filter((room) => {
- return room.getMyMembership() && (room.getMyMembership() == "invite" || room.getMyMembership() == "join") && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0;
+ return room.getMyMembership() && ["invite", "join", "knock"].includes(room.getMyMembership()) && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0;
});
updatedRooms.forEach((room) => {
room["avatar"] = room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true, this.useAuthedMedia);
@@ -692,6 +716,14 @@ export default {
}
},
+ answerKnock(roomId, userId, allowAccess, reason) {
+ if (allowAccess) {
+ this.matrixClient.invite(roomId, userId, reason);
+ } else {
+ this.matrixClient.kick(roomId, userId, reason);
+ }
+ },
+
/**
* Returns true if the current user is joined to the given room.
* @param roomIdOrAlias
@@ -921,7 +953,8 @@ export default {
statusCallback(this.$t("room.purge_removing_members"));
var joined = room.getMembersWithMembership("join");
var invited = room.getMembersWithMembership("invite");
- var allMembers = joined.concat(invited);
+ var knocked = room.getMembersWithMembership("knock");
+ var allMembers = joined.concat(invited).concat(knocked);
const me = allMembers.find((m) => m.userId == self.currentUserId);