-
+
file: {{ currentImageInputPath.name }}
- {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}
+ {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}
- {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
+ {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
- ({{ formatBytes(currentImageInput.scaledSize) }})
+ ({{ formatBytes(currentImageInput.scaledSize) }})
({{ formatBytes(currentImageInputPath.size) }})
-
+
{{ currentSendError }}
{{ currentSendProgress }}
@@ -321,17 +210,10 @@
{{
- $t("menu.cancel")
+ $t("menu.cancel")
}}
- {{ $t("menu.send") }}
+ {{ $t("menu.send") }}
@@ -363,7 +245,7 @@
{{
- $t("menu.ok")
+ $t("menu.ok")
}}
@@ -389,6 +271,7 @@ import BottomSheet from "./BottomSheet.vue";
import ImageResize from "image-resize";
import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin";
+import AudioLayout from "./AudioLayout.vue";
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
@@ -405,7 +288,7 @@ function ScrollPosition(node) {
this.readyFor = "up";
}
-ScrollPosition.prototype.restore = function() {
+ScrollPosition.prototype.restore = function () {
if (this.readyFor === "up") {
this.node.scrollTop = this.node.scrollHeight - this.previousScrollHeightMinusTop;
} else {
@@ -413,7 +296,7 @@ ScrollPosition.prototype.restore = function() {
}
};
-ScrollPosition.prototype.prepareFor = function(direction) {
+ScrollPosition.prototype.prepareFor = function (direction) {
this.readyFor = direction || "up";
if (this.readyFor === "up") {
this.previousScrollHeightMinusTop = this.node.scrollHeight - this.node.scrollTop;
@@ -436,6 +319,7 @@ export default {
BottomSheet,
AvatarOperations,
CreatePollDialog,
+ AudioLayout
},
data() {
@@ -516,9 +400,13 @@ export default {
},
mounted() {
- const container = this.$refs.chatContainer;
- this.scrollPosition = new ScrollPosition(container);
- this.chatContainerSize = this.$refs.chatContainerResizer.$el.clientHeight;
+ const container = this.chatContainer;
+ if (container) {
+ this.scrollPosition = new ScrollPosition(container);
+ if (this.$refs.chatContainerResizer) {
+ this.chatContainerSize = this.$refs.chatContainerResizer.$el.clientHeight;
+ }
+ }
},
beforeDestroy() {
@@ -531,6 +419,14 @@ export default {
},
computed: {
+ chatContainer() {
+ const container = this.$refs.chatContainer;
+ console.log("GOT CONTAINER", container);
+ if (this.useVoiceMode) {
+ return container.$el;
+ }
+ return container;
+ },
senderDisplayName() {
return this.room.getMember(this.replyToEvent.sender.userId).name;
},
@@ -615,7 +511,7 @@ export default {
return util.browserCanRecordAudio();
},
debugging() {
- return (window.location.host || "").startsWith("localhost");
+ return false; //(window.location.host || "").startsWith("localhost");
},
invitationCount() {
return this.$matrix.invites.length;
@@ -627,9 +523,23 @@ export default {
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
return isAdmin;
},
+ useVoiceMode: {
+ get: function () {
+ if (!this.$config.experimental_voice_mode) return false;
+ return util.useVoiceMode(this.room);
+ },
+ },
},
watch: {
+ initialLoadDone: {
+ immediate: true,
+ handler(value, oldValue) {
+ if (value && !oldValue) {
+ console.log("Loading finished!");
+ }
+ }
+ },
roomId: {
immediate: true,
handler(value, oldValue) {
@@ -697,6 +607,12 @@ export default {
});
}
},
+ showRecorder(show) {
+ if (this.useVoiceMode) {
+ // Send typing indicators when recorder UI is opened/closed
+ this.$matrix.matrixClient.sendTyping(this.roomId, show, 10 * 60 * 1000);
+ }
+ }
},
methods: {
@@ -729,6 +645,7 @@ export default {
const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer;
if (
+ container &&
container.scrollHeight <= (1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
self.timelineWindow &&
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
@@ -867,22 +784,22 @@ export default {
handleChatContainerResize({ ignoredWidth, height }) {
const delta = height - this.chatContainerSize;
this.chatContainerSize = height;
- const container = this.$refs.chatContainer;
- if (delta < 0) {
+ const container = this.chatContainer;
+ if (container && delta < 0) {
container.scrollTop -= delta;
}
},
paginateBackIfNeeded() {
this.$nextTick(() => {
- const container = this.$refs.chatContainer;
- if (container.scrollHeight <= container.clientHeight) {
+ const container = this.chatContainer;
+ if (container && container.scrollHeight <= container.clientHeight) {
this.handleScrolledToTop();
}
});
},
onScroll(ignoredevent) {
- const container = this.$refs.chatContainer;
+ const container = this.chatContainer;
if (!container) {
return;
}
@@ -898,7 +815,7 @@ export default {
container.scrollHeight === container.clientHeight
? false
: container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight ||
- (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
+ (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
this.restartRRTimer();
},
@@ -908,19 +825,20 @@ export default {
return; // Not for this room
}
+ const loadingDone = this.initialLoadDone;
this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
- if (this.initialLoadDone) {
+ if (this.initialLoadDone && !this.useVoiceMode) {
this.paginateBackIfNeeded();
}
- // If we are at bottom, scroll to see new events...
- const container = this.$refs.chatContainer;
- var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
- if (container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
- scrollToSeeNew = true;
- }
- if (this.initialLoadDone && event.forwardLooking && !event.isRelation()) {
+ if (loadingDone && event.forwardLooking && !event.isRelation()) {
+ // If we are at bottom, scroll to see new events...
+ var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
+ const container = this.chatContainer;
+ if (container && container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
+ scrollToSeeNew = true;
+ }
this.handleScrolledToBottom(scrollToSeeNew);
}
},
@@ -1102,7 +1020,6 @@ export default {
},
handleScrolledToTop() {
- console.log("@top");
if (
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) &&
@@ -1129,7 +1046,6 @@ export default {
},
handleScrolledToBottom(scrollToEnd) {
- console.log("@bottom");
if (
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS) &&
@@ -1140,16 +1056,18 @@ export default {
.paginate(EventTimeline.FORWARDS, 10, true)
.then((success) => {
if (success) {
- this.scrollPosition.prepareFor("down");
this.events = this.timelineWindow.getEvents();
- this.$nextTick(() => {
- // restore scroll position!
- console.log("Restore scroll!");
- this.scrollPosition.restore();
- if (scrollToEnd) {
- this.smoothScrollToEnd();
- }
- });
+ if (!this.useVoiceMode) {
+ this.scrollPosition.prepareFor("down");
+ this.$nextTick(() => {
+ // restore scroll position!
+ console.log("Restore scroll!");
+ this.scrollPosition.restore();
+ if (scrollToEnd) {
+ this.smoothScrollToEnd();
+ }
+ });
+ }
}
})
.finally(() => {
@@ -1162,7 +1080,7 @@ export default {
* Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom.
*/
scrollToEvent(eventId) {
- const container = this.$refs.chatContainer;
+ const container = this.chatContainer;
const ref = this.$refs[eventId];
if (container && ref) {
const targetY = container.clientHeight / 2;
@@ -1172,9 +1090,9 @@ export default {
},
smoothScrollToEnd() {
- this.$nextTick(function() {
- const container = this.$refs.chatContainer;
- if (container.children.length > 0) {
+ this.$nextTick(function () {
+ const container = this.chatContainer;
+ if (container && container.children.length > 0) {
const lastChild = container.children[container.children.length - 1];
console.log("Scroll into view", lastChild);
window.requestAnimationFrame(() => {
@@ -1251,7 +1169,7 @@ export default {
document.body.appendChild(link);
link.click();
- setTimeout(function() {
+ setTimeout(function () {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 200);
@@ -1268,8 +1186,8 @@ export default {
},
emojiSelected(e) {
- if(this.isEmojiQuickReaction) {
- // When quick emoji picker is clicked
+ if (this.isEmojiQuickReaction) {
+ // When quick emoji picker is clicked
if (this.selectedEvent) {
const event = this.selectedEvent;
this.selectedEvent = null;
@@ -1286,6 +1204,24 @@ export default {
},
sendQuickReaction(e) {
+ let previousReaction = null;
+
+ // Figure out if we have already sent this emoji, in that case redact it again (toggle)
+ //
+ const reactions = this.timelineSet.relations.getChildEventsForEvent(e.event.getId(), 'm.annotation', 'm.reaction');
+ if (reactions && reactions._eventsCount > 0) {
+ const relations = reactions.getRelations();
+ for (const r of relations) {
+ const emoji = r.getRelation().key;
+ const sender = r.getSender();
+ if (emoji == e.reaction && sender == this.$matrix.currentUserId) {
+ previousReaction = r.isRedacted() ? null : r;
+ }
+ }
+ }
+ if (previousReaction) {
+ this.redact(previousReaction);
+ } else {
util
.sendQuickReaction(this.$matrix.matrixClient, this.roomId, e.reaction, e.event)
.then(() => {
@@ -1294,6 +1230,7 @@ export default {
.catch((err) => {
console.log("Failed to send quick reaction:", err);
});
+ }
},
sendSticker(stickerShortCode) {
@@ -1370,64 +1307,79 @@ export default {
*/
restartRRTimer() {
this.stopRRTimer();
- this.rrTimer = setTimeout(this.rrTimerElapsed, READ_RECEIPT_TIMEOUT);
+
+ if (this.$matrix.currentRoomBeingPurged) {
+ return;
+ }
+
+ let eventIdFirst = null;
+ let eventIdLast = null;
+ if (!this.useVoiceMode) {
+ const container = this.chatContainer;
+ const elFirst = util.getFirstVisibleElement(container, (item) => item.hasAttribute("eventId"));
+ const elLast = util.getLastVisibleElement(container, (item) => item.hasAttribute("eventId"));
+ if (elFirst && elLast) {
+ eventIdFirst = elFirst.getAttribute("eventId");
+ eventIdLast = elLast.getAttribute("eventId");
+ }
+ }
+ if (eventIdFirst && eventIdLast) {
+ this.rrTimer = setTimeout(() => { this.rrTimerElapsed(eventIdFirst, eventIdLast) }, READ_RECEIPT_TIMEOUT);
+ }
},
- rrTimerElapsed() {
+ rrTimerElapsed(eventIdFirst, eventIdLast) {
this.rrTimer = null;
+ this.sendRR(eventIdFirst, eventIdLast);
+ this.restartRRTimer();
+ },
- const container = this.$refs.chatContainer;
- const elFirst = util.getFirstVisibleElement(container);
- const elLast = util.getLastVisibleElement(container);
- if (elFirst && elLast) {
- const eventIdFirst = elFirst.getAttribute("eventId");
- const eventIdLast = elLast.getAttribute("eventId");
- if (eventIdLast && this.room) {
- var event = this.room.findEventById(eventIdLast);
- const index = this.events.indexOf(event);
+ sendRR(eventIdFirst, eventIdLast) {
+ console.log("SEND RR", eventIdFirst, eventIdLast);
+ if (eventIdLast && this.room) {
+ var event = this.room.findEventById(eventIdLast);
+ const index = this.events.indexOf(event);
- // Walk backwards through visible events to the first one that is incoming
- //
- var lastTimestamp = 0;
- if (this.lastRR) {
- lastTimestamp = this.lastRR.getTs();
+ // Walk backwards through visible events to the first one that is incoming
+ //
+ var lastTimestamp = 0;
+ if (this.lastRR) {
+ lastTimestamp = this.lastRR.getTs();
+ }
+
+ for (var i = index; i >= 0; i--) {
+ event = this.events[i];
+ if (event == this.lastRR || event.getTs() <= lastTimestamp) {
+ // Already sent this or too old...
+ break;
+ }
+ // Make sure it's not a local echo event...
+ if (!event.getId().startsWith("~")) {
+ // Send read receipt
+ this.$matrix.matrixClient
+ .sendReadReceipt(event)
+ .then(() => {
+ this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
+ })
+ .then(() => {
+ console.log("RR sent for event: " + event.getId());
+ this.lastRR = event;
+ })
+ .catch((err) => {
+ console.log("Failed to update read marker: ", err);
+ })
+ .finally(() => {
+ this.restartRRTimer();
+ });
+ return; // Bail out here
}
- for (var i = index; i >= 0; i--) {
- event = this.events[i];
- if (event == this.lastRR || event.getTs() <= lastTimestamp) {
- // Already sent this or too old...
- break;
- }
- // Make sure it's not a local echo event...
- if (!event.getId().startsWith("~")) {
- // Send read receipt
- this.$matrix.matrixClient
- .sendReadReceipt(event)
- .then(() => {
- this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
- })
- .then(() => {
- console.log("RR sent for event: " + event.getId());
- this.lastRR = event;
- })
- .catch((err) => {
- console.log("Failed to update read marker: ", err);
- })
- .finally(() => {
- this.restartRRTimer();
- });
- return; // Bail out here
- }
-
- // Stop iterating at first visible
- if (event.getId() == eventIdFirst) {
- break;
- }
+ // Stop iterating at first visible
+ if (event.getId() == eventIdFirst) {
+ break;
}
}
}
- this.restartRRTimer();
},
showRecordingUI() {
@@ -1499,9 +1451,9 @@ export default {
let div = document.createElement("div");
div.classList.add("toast");
div.innerText = this.$t("poll_create.results_shared");
- this.$refs.chatContainer.parentElement.appendChild(div);
+ this.chatContainer.parentElement.appendChild(div);
setTimeout(() => {
- this.$refs.chatContainer.parentElement.removeChild(div);
+ this.chatContainer.parentElement.removeChild(div);
}, 3000);
}
},
diff --git a/src/components/ChatHeader.vue b/src/components/ChatHeader.vue
index 79cfb91..4d0d703 100644
--- a/src/components/ChatHeader.vue
+++ b/src/components/ChatHeader.vue
@@ -16,7 +16,8 @@
@click.stop="onHeaderClicked"
>
- {{ room.name }}
+ {{ room.name }}
$vuetify.icons.ic_dropdown
+
{{ $tc("room.members", memberCount) }}
@@ -25,27 +26,32 @@
- {{ $t("room_info.purge") }}
+ $vuetify.icons.ic_moderator-delete
- {{ $t("room.leave") }}
+ $vuetify.icons.ic_member-leave
@@ -132,6 +138,9 @@ export default {
}
}
return null;
+ },
+ notifications() {
+ return this.$matrix.joinedRooms.some(room => room.roomId !== this.$matrix.currentRoomId && room.getUnreadNotificationCount("total") > 0);
}
},
watch: {
diff --git a/src/components/CreateRoom.vue b/src/components/CreateRoom.vue
index 3332e2a..85d08ea 100644
--- a/src/components/CreateRoom.vue
+++ b/src/components/CreateRoom.vue
@@ -3,29 +3,23 @@
{{ $t("new_room.new_room") }}
- -->
@@ -39,51 +33,43 @@
-
+
{{ $t("new_room.name_room") }}
-
- {{ $t("new_room.room_topic") }}
-
- {{roomCreationErrorMsg}}
-
-
- {{ status }}
-
-
- {{ $t("new_room.create") }}
-
+
+ {{ $t("new_room.room_topic") }}
+
+
+
+
+
+
{{ $t("new_room.options") }}
+
expand_more
+
expand_less
+
+
+
+
+
{{ $t('room_info.voice_mode') }}
+
{{ $t('room_info.voice_mode_info') }}
+
+
+
+
+
+
+ {{ roomCreationErrorMsg }}
+
+
+ {{ status }}
+
+
+ {{ $t("new_room.create") }}
+
@@ -91,126 +77,101 @@
+
+ {{ $t("new_room.link_copied") }}
+
+ -->
+
+ {{ status }}
+ -->
-
+
+
{{ $t("join.choose_name") }}
-
+
-
+
@@ -219,33 +180,23 @@
{{ data.item.name }}
-
-
-
+
+
+
-
+
-
- {{ $t("join.enter_room") }}
-
+
+ {{ $t("join.enter_room") }}
+
@@ -253,7 +204,7 @@
diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js
index 970fe16..d87a548 100644
--- a/src/components/chatMixin.js
+++ b/src/components/chatMixin.js
@@ -22,6 +22,8 @@ import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoEx
import ContactJoin from "./messages/ContactJoin.vue";
import ContactLeave from "./messages/ContactLeave.vue";
import ContactInvited from "./messages/ContactInvited.vue";
+import ContactKicked from "./messages/ContactKicked.vue";
+import ContactBanned from "./messages/ContactBanned.vue";
import ContactChanged from "./messages/ContactChanged.vue";
import RoomCreated from "./messages/RoomCreated.vue";
import RoomAliased from "./messages/RoomAliased.vue";
@@ -66,6 +68,8 @@ export default {
ContactJoin,
ContactLeave,
ContactInvited,
+ ContactKicked,
+ ContactBanned,
ContactChanged,
RoomCreated,
RoomAliased,
@@ -123,9 +127,15 @@ export default {
return ContactJoin;
}
} else if (event.getContent().membership == "leave") {
+ if ((event.getPrevContent() || {}).membership == "join" &&
+ event.getStateKey() != event.getSender()) {
+ return ContactKicked;
+ }
return ContactLeave;
} else if (event.getContent().membership == "invite") {
return ContactInvited;
+ } else if (event.getContent().membership == "ban") {
+ return ContactBanned;
}
break;
diff --git a/src/components/messages/AudioPlayer.vue b/src/components/messages/AudioPlayer.vue
index 00157aa..b5c0815 100644
--- a/src/components/messages/AudioPlayer.vue
+++ b/src/components/messages/AudioPlayer.vue
@@ -54,9 +54,10 @@ export default {
this.player.addEventListener("pause", () => {
this.playing = false;
});
- this.player.addEventListener("ended", function () {
+ this.player.addEventListener("ended", () => {
this.pause();
this.playing = false;
+ this.$emit("playback-ended");
});
},
beforeDestroy() {
diff --git a/src/components/messages/ContactBanned.vue b/src/components/messages/ContactBanned.vue
new file mode 100644
index 0000000..37c2a45
--- /dev/null
+++ b/src/components/messages/ContactBanned.vue
@@ -0,0 +1,25 @@
+
+
+
+ {{ (event.getStateKey() == this.$matrix.currentUserId) ?
+ $t('message.user_was_banned_you')
+ :
+ event.getSender() == this.$matrix.currentUserId ?
+ $t('message.user_was_banned_by_you',{user: eventStateKeyDisplayName(event)})
+ :
+ $t('message.user_was_banned',{user: eventStateKeyDisplayName(event)})
+ }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/messages/ContactChanged.vue b/src/components/messages/ContactChanged.vue
index 95622dc..e03d24a 100644
--- a/src/components/messages/ContactChanged.vue
+++ b/src/components/messages/ContactChanged.vue
@@ -33,7 +33,7 @@ export default {
if (this.displayNameChange) {
return this.event.getPrevContent().displayname;
}
- return this.stateEventDisplayName(this.event);
+ return this.eventStateKeyDisplayName(this.event);
},
}
};
diff --git a/src/components/messages/ContactInvited.vue b/src/components/messages/ContactInvited.vue
index 85f96a8..110c43e 100644
--- a/src/components/messages/ContactInvited.vue
+++ b/src/components/messages/ContactInvited.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_was_invited', {user: event.getContent().displayname || stateEventDisplayName(event)}) }}
+ {{ $t('message.user_was_invited', {user: event.getContent().displayname || eventStateKeyDisplayName(event)}) }}
diff --git a/src/components/messages/ContactJoin.vue b/src/components/messages/ContactJoin.vue
index b8bce13..d8aa6d8 100644
--- a/src/components/messages/ContactJoin.vue
+++ b/src/components/messages/ContactJoin.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_joined',{user: stateEventDisplayName(event)}) }}
+ {{ $t('message.user_joined',{user: eventSenderDisplayName(event)}) }}
diff --git a/src/components/messages/ContactKicked.vue b/src/components/messages/ContactKicked.vue
new file mode 100644
index 0000000..b34da00
--- /dev/null
+++ b/src/components/messages/ContactKicked.vue
@@ -0,0 +1,25 @@
+
+
+
+ {{ (event.getStateKey() == this.$matrix.currentUserId) ?
+ $t('message.user_was_kicked_you')
+ :
+ event.getSender() == this.$matrix.currentUserId ?
+ $t('message.user_was_kicked_by_you',{user: eventStateKeyDisplayName(event)})
+ :
+ $t('message.user_was_kicked',{user: eventStateKeyDisplayName(event)})
+ }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/messages/ContactLeave.vue b/src/components/messages/ContactLeave.vue
index 0b77831..1e40799 100644
--- a/src/components/messages/ContactLeave.vue
+++ b/src/components/messages/ContactLeave.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_left',{user: stateEventDisplayName(event)}) }}
+ {{ $t('message.user_left',{user: eventStateKeyDisplayName(event)}) }}
diff --git a/src/components/messages/MessageIncoming.vue b/src/components/messages/MessageIncoming.vue
index 9c31eb3..e1f1727 100644
--- a/src/components/messages/MessageIncoming.vue
+++ b/src/components/messages/MessageIncoming.vue
@@ -2,7 +2,7 @@
-
{{ messageEventDisplayName(event) }}
+
{{ eventSenderDisplayName(event) }}
{{ formatTime(event.event.origin_server_ts) }}
@@ -10,7 +10,7 @@
{{
- messageEventDisplayName(event).substring(0, 1).toUpperCase()
+ eventSenderDisplayName(event).substring(0, 1).toUpperCase()
}}
diff --git a/src/components/messages/QuickReactions.vue b/src/components/messages/QuickReactions.vue
index baa69ce..63dc6be 100644
--- a/src/components/messages/QuickReactions.vue
+++ b/src/components/messages/QuickReactions.vue
@@ -1,6 +1,6 @@
-
+
{{ name }} {{ value.length }}
@@ -48,6 +48,12 @@ export default {
onAddRelation(ignoredevent) {
this.processReactions();
},
+ onRemoveRelation(ignoredevent) {
+ this.processReactions();
+ },
+ onRedactRelation(ignoredevent) {
+ this.processReactions();
+ },
processReactions() {
var reactionMap = {};
if (this.reactions && this.reactions._eventsCount > 0) {
@@ -57,10 +63,13 @@ export default {
const sender = r.getSender();
if (reactionMap[emoji]) {
const array = reactionMap[emoji];
+ if (r.isRedacted()) {
+ delete array[sender];
+ }
if (!array.includes(sender)) {
array.push(sender)
}
- } else {
+ } else if (!r.isRedacted()) {
reactionMap[emoji] = [sender];
}
}
@@ -73,9 +82,13 @@ export default {
handler(newValue, oldValue) {
if (oldValue) {
oldValue.off('Relations.add', this.onAddRelation);
+ oldValue.off('Relations.remove', this.onRemoveRelation);
+ oldValue.off('Relations.redaction', this.onRedactRelation);
}
if (newValue) {
newValue.on('Relations.add', this.onAddRelation);
+ newValue.on('Relations.remove', this.onRemoveRelation);
+ newValue.on('Relations.redaction', this.onRedactRelation);
}
this.processReactions();
},
diff --git a/src/components/messages/RoomAliased.vue b/src/components/messages/RoomAliased.vue
index 5a908e6..9c87f29 100644
--- a/src/components/messages/RoomAliased.vue
+++ b/src/components/messages/RoomAliased.vue
@@ -1,6 +1,6 @@
- {{ $t('message.user_aliased_room', {user: stateEventDisplayName(event), alias: event.getContent().alias}) }}
+ {{ $t('message.user_aliased_room', {user: eventSenderDisplayName(event), alias: event.getContent().alias}) }}
diff --git a/src/components/messages/RoomAvatarChanged.vue b/src/components/messages/RoomAvatarChanged.vue
index 7645e80..cc68bca 100644
--- a/src/components/messages/RoomAvatarChanged.vue
+++ b/src/components/messages/RoomAvatarChanged.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_changed_room_avatar',{user: stateEventDisplayName(event)}) }}
+ {{ $t('message.user_changed_room_avatar',{user: eventSenderDisplayName(event)}) }}
diff --git a/src/components/messages/RoomCreated.vue b/src/components/messages/RoomCreated.vue
index 9603e0f..c83ad15 100644
--- a/src/components/messages/RoomCreated.vue
+++ b/src/components/messages/RoomCreated.vue
@@ -1,6 +1,6 @@
- {{ $t('message.user_created_room', {user: stateEventDisplayName(event)}) }}
+ {{ $t('message.user_created_room', {user: eventSenderDisplayName(event)}) }}
diff --git a/src/components/messages/RoomDeletionNotice.vue b/src/components/messages/RoomDeletionNotice.vue
index 9a2f476..f8b7524 100644
--- a/src/components/messages/RoomDeletionNotice.vue
+++ b/src/components/messages/RoomDeletionNotice.vue
@@ -4,7 +4,7 @@
👋
{{
$t("purge_room.room_deletion_notice", {
- user: stateEventDisplayName(event),
+ user: eventSenderDisplayName(event),
})
}}
diff --git a/src/components/messages/RoomEncrypted.vue b/src/components/messages/RoomEncrypted.vue
index 669b6e7..874d989 100644
--- a/src/components/messages/RoomEncrypted.vue
+++ b/src/components/messages/RoomEncrypted.vue
@@ -1,6 +1,6 @@
- {{ $t('message.user_encrypted_room', {user: stateEventDisplayName(event)}) }}
+ {{ $t('message.user_encrypted_room', {user: eventSenderDisplayName(event)}) }}
diff --git a/src/components/messages/RoomGuestAccessChanged.vue b/src/components/messages/RoomGuestAccessChanged.vue
index 13d7f27..b89501c 100644
--- a/src/components/messages/RoomGuestAccessChanged.vue
+++ b/src/components/messages/RoomGuestAccessChanged.vue
@@ -3,10 +3,10 @@
{{
openToGuests
? $t("message.user_changed_guest_access_open", {
- user: stateEventDisplayName(event),
+ user: eventSenderDisplayName(event),
})
: $t("message.user_changed_guest_access_closed", {
- user: stateEventDisplayName(event),
+ user: eventSenderDisplayName(event),
})
}}
diff --git a/src/components/messages/RoomHistoryVisibility.vue b/src/components/messages/RoomHistoryVisibility.vue
index 19892f4..a6e1592 100644
--- a/src/components/messages/RoomHistoryVisibility.vue
+++ b/src/components/messages/RoomHistoryVisibility.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_changed_room_history',{user: stateEventDisplayName(event), type: history(event)}) }}
+ {{ $t('message.user_changed_room_history',{user: eventSenderDisplayName(event), type: history(event)}) }}
diff --git a/src/components/messages/RoomJoinRules.vue b/src/components/messages/RoomJoinRules.vue
index f80dbff..0ee433b 100644
--- a/src/components/messages/RoomJoinRules.vue
+++ b/src/components/messages/RoomJoinRules.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_changed_join_rules', { user: stateEventDisplayName(event), type: joinRule(event)}) }}
+ {{ $t('message.user_changed_join_rules', { user: eventSenderDisplayName(event), type: joinRule(event)}) }}
diff --git a/src/components/messages/RoomNameChanged.vue b/src/components/messages/RoomNameChanged.vue
index ba152b8..1e844c5 100644
--- a/src/components/messages/RoomNameChanged.vue
+++ b/src/components/messages/RoomNameChanged.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_changed_room_name', {user: stateEventDisplayName(event), name: event.getContent().name}) }}
+ {{ $t('message.user_changed_room_name', {user: eventSenderDisplayName(event), name: event.getContent().name}) }}
diff --git a/src/components/messages/RoomTopicChanged.vue b/src/components/messages/RoomTopicChanged.vue
index 6ac9992..9268756 100644
--- a/src/components/messages/RoomTopicChanged.vue
+++ b/src/components/messages/RoomTopicChanged.vue
@@ -1,7 +1,7 @@
- {{ $t('message.user_changed_room_topic', {user: stateEventDisplayName(event), topic: event.getContent().topic}) }}
+ {{ $t('message.user_changed_room_topic', {user: eventSenderDisplayName(event), topic: event.getContent().topic}) }}
diff --git a/src/components/messages/attachmentMixin.js b/src/components/messages/attachmentMixin.js
index 6a5a1fa..06b39c5 100644
--- a/src/components/messages/attachmentMixin.js
+++ b/src/components/messages/attachmentMixin.js
@@ -7,25 +7,41 @@ export default {
downloadProgress: null
}
},
- mounted() {
- console.log("Mounted with event:", JSON.stringify(this.event.getContent()))
- util
- .getAttachment(this.$matrix.matrixClient, this.event, (progress) => {
- this.downloadProgress = progress;
- console.log("Progress: " + progress);
- })
- .then((url) => {
- this.src = url;
- })
- .catch((err) => {
- console.log("Failed to fetch attachment: ", err);
- });
- },
- beforeDestroy() {
- if (this.src) {
- const objectUrl = this.src;
- this.src = null;
- URL.revokeObjectURL(objectUrl);
+ watch: {
+ event: {
+ immediate: false,
+ handler(value, ignoredOldValue) {
+ this.loadAttachmentSource(value);
+ }
}
},
+ mounted() {
+ this.loadAttachmentSource(this.event);
+ },
+ beforeDestroy() {
+ this.loadAttachmentSource(null); // Release
+ },
+ methods: {
+ loadAttachmentSource(event) {
+ if (this.src) {
+ const objectUrl = this.src;
+ this.src = null;
+ URL.revokeObjectURL(objectUrl);
+ }
+ if (event) {
+ util
+ .getAttachment(this.$matrix.matrixClient, event, (progress) => {
+ this.downloadProgress = progress;
+ console.log("Progress: " + progress);
+ })
+ .then((url) => {
+ this.src = url;
+ })
+ .catch((err) => {
+ console.log("Failed to fetch attachment: ", err);
+ });
+
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/components/messages/messageMixin.js b/src/components/messages/messageMixin.js
index 1820781..e1c9046 100644
--- a/src/components/messages/messageMixin.js
+++ b/src/components/messages/messageMixin.js
@@ -1,61 +1,83 @@
-import QuickReactions from './QuickReactions.vue';
-var linkify = require('linkifyjs');
-var linkifyHtml = require('linkifyjs/html');
+import QuickReactions from "./QuickReactions.vue";
+import * as linkify from 'linkifyjs';
+import linkifyHtml from 'linkify-html';
linkify.options.defaults.className = "link";
-linkify.options.defaults.target = { url: '_blank' };
+linkify.options.defaults.target = { url: "_blank" };
export default {
components: {
- QuickReactions
+ QuickReactions,
},
props: {
room: {
type: Object,
default: function () {
- return null
- }
+ return null;
+ },
},
- event: {
+ originalEvent: {
type: Object,
default: function () {
- return {}
- }
+ return {};
+ },
},
nextEvent: {
type: Object,
default: function () {
- return null
- }
+ return null;
+ },
},
timelineSet: {
type: Object,
default: function () {
- return null
- }
+ return null;
+ },
},
},
data() {
return {
+ event: {},
inReplyToEvent: null,
- inReplyToSender: null
- }
+ inReplyToSender: null,
+ };
},
mounted() {
- const relatesTo = this.event.getWireContent()['m.relates_to'];
- if (relatesTo && relatesTo['m.in_reply_to'])
- {
+ const relatesTo = this.validEvent && this.event.getWireContent()["m.relates_to"];
+ if (relatesTo && relatesTo["m.in_reply_to"]) {
// Can we find the original message?
- const originalEventId = relatesTo['m.in_reply_to'].event_id;
+ const originalEventId = relatesTo["m.in_reply_to"].event_id;
if (originalEventId && this.timelineSet) {
const originalEvent = this.timelineSet.findEventById(originalEventId);
if (originalEvent) {
this.inReplyToEvent = originalEvent;
- this.inReplyToSender = this.messageEventDisplayName(originalEvent);
+ this.inReplyToSender = this.eventSenderDisplayName(originalEvent);
}
}
}
},
+ watch: {
+ originalEvent: {
+ immediate: true,
+ handler(originalEvent, ignoredOldValue) {
+ this.event = originalEvent;
+ // Check not null and not {}
+ if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) {
+ this.originalEvent.getDecryptionPromise().then(() => {
+ this.event = originalEvent;
+ });
+ }
+ },
+ },
+ },
computed: {
+ /**
+ *
+ * @returns true if event is non-null and contains data
+ */
+ validEvent() {
+ return this.event && Object.keys(this.event).length !== 0;
+ },
+
incoming() {
return this.event && this.event.getSender() != this.$matrix.currentUserId;
},
@@ -67,21 +89,20 @@ export default {
if (this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
const ts1 = this.nextEvent.event.origin_server_ts;
const ts2 = this.event.event.origin_server_ts;
- return (ts1 - ts2) < (2 * 60 * 1000); // less than 2 minutes
+ return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
}
return true;
},
inReplyToText() {
- const relatesTo = this.event.getWireContent()['m.relates_to'];
- if (relatesTo && relatesTo['m.in_reply_to'])
- {
+ const relatesTo = this.event.getWireContent()["m.relates_to"];
+ if (relatesTo && relatesTo["m.in_reply_to"]) {
const content = this.event.getContent();
- const lines = content.body.split('\n').reverse();
- while (lines.length && !lines[0].startsWith('> ')) lines.shift();
+ const lines = content.body.split("\n").reverse();
+ while (lines.length && !lines[0].startsWith("> ")) lines.shift();
// Reply fallback has a blank line after it, so remove it to prevent leading newline
- if (lines[0] === '') lines.shift();
- const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, '');
+ if (lines[0] === "") lines.shift();
+ const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
if (text) {
return text;
}
@@ -92,23 +113,22 @@ export default {
}
// We don't have the original text (at the moment at least)
- return this.$t('fallbacks.original_text');
+ return this.$t("fallbacks.original_text");
}
return null;
},
messageText() {
- const relatesTo = this.event.getWireContent()['m.relates_to'];
- if (relatesTo && relatesTo['m.in_reply_to'])
- {
+ const relatesTo = this.event.getWireContent()["m.relates_to"];
+ if (relatesTo && relatesTo["m.in_reply_to"]) {
const content = this.event.getContent();
// Remove the new text and strip "> " from the old original text
- const lines = content.body.split('\n');
- while (lines.length && lines[0].startsWith('> ')) lines.shift();
+ const lines = content.body.split("\n");
+ while (lines.length && lines[0].startsWith("> ")) lines.shift();
// Reply fallback has a blank line after it, so remove it to prevent leading newline
- if (lines[0] === '') lines.shift();
- return lines.join('\n');
+ if (lines[0] === "") lines.shift();
+ return lines.join("\n");
}
return this.event.getContent().body;
},
@@ -118,48 +138,44 @@ export default {
*/
messageClasses() {
- return {'messageIn':true,'from-admin':this.senderIsAdminOrModerator(this.event)}
+ return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event) };
},
userAvatar() {
if (!this.$matrix.userAvatar) {
return null;
}
- return this.$matrix.matrixClient.mxcUrlToHttp(
- this.$matrix.userAvatar,
- 80,
- 80,
- "scale",
- true
- );
+ return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true);
},
userAvatarLetter() {
if (!this.$matrix.currentUser) {
return null;
}
- return (this.$matrix.currentUserDisplayName || this.$matrix.currentUserId.substring(1)).substring(0, 1).toUpperCase();
- }
+ return (this.$matrix.currentUserDisplayName || this.$matrix.currentUserId.substring(1))
+ .substring(0, 1)
+ .toUpperCase();
+ },
},
methods: {
ownAvatarClicked() {
- this.$emit("own-avatar-clicked", {event: this.event});
+ this.$emit("own-avatar-clicked", { event: this.event });
},
otherAvatarClicked(avatarRef) {
- this.$emit("other-avatar-clicked", {event: this.event, anchor: avatarRef});
+ this.$emit("other-avatar-clicked", { event: this.event, anchor: avatarRef });
},
showContextMenu(buttonRef) {
- this.$emit("context-menu", {event: this.event,anchor: buttonRef});
+ this.$emit("context-menu", { event: this.event, anchor: buttonRef });
},
/**
* Get a display name given an event.
*/
- stateEventDisplayName(event) {
+ eventSenderDisplayName(event) {
if (event.getSender() == this.$matrix.currentUserId) {
- return this.$t('message.you');
+ return this.$t("message.you");
}
if (this.room) {
const member = this.room.getMember(event.getSender());
@@ -167,24 +183,33 @@ export default {
return member.name;
}
}
- return event.getContent().displayname || event.event.state_key;
+ return event.getContent().displayname || event.getSender();
},
- messageEventDisplayName(event) {
- return this.stateEventDisplayName(event);
+ /**
+ * In the case where the state_key points out a userId for an operation (e.g. membership events)
+ * return the display name of the affected user.
+ * @param event
+ * @returns
+ */
+ eventStateKeyDisplayName(event) {
+ if (event.getStateKey() == this.$matrix.currentUserId) {
+ return this.$t("message.you");
+ }
+ if (this.room) {
+ const member = this.room.getMember(event.getStateKey());
+ if (member) {
+ return member.name;
+ }
+ }
+ return event.getStateKey();
},
messageEventAvatar(event) {
if (this.room) {
const member = this.room.getMember(event.getSender());
if (member) {
- return member.getAvatarUrl(
- this.$matrix.matrixClient.getHomeserverUrl(),
- 40,
- 40,
- "scale",
- true
- );
+ return member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true);
}
}
return null;
@@ -221,6 +246,6 @@ export default {
linkify(text) {
return linkifyHtml(text);
- }
+ },
},
-}
\ No newline at end of file
+};
diff --git a/src/main.js b/src/main.js
index 82db06b..9172559 100644
--- a/src/main.js
+++ b/src/main.js
@@ -26,7 +26,8 @@ Vue.config.productionTip = false
Vue.use(VueResize);
Vue.use(VEmojiPicker);
Vue.use(matrix, { store: store, i18n: i18n });
-Vue.use(config); // Use this before cleaninsights below, it depends on config!
+// eslint-disable-next-line
+Vue.use(config, globalThis.window.location.origin); // Use this before cleaninsights below, it depends on config!
Vue.use(analytics);
Vue.use(VueClipboard);
@@ -36,10 +37,18 @@ Vue.use((Vue) => {
Vue.prototype.$bubble = function $bubble(eventName, ...args) {
// Emit the event on all parent components
let component = this;
+ let arg = args.at(0);
+ let stop = false;
+ if (arg) {
+ // Add a "stopPropagation" function so that we can do v-on:.stop="..."
+ arg.stopPropagation = () => {
+ stop = true;
+ }
+ }
do {
- component.$emit(eventName, ...args);
+ component.$emit(eventName, ... args);
component = component.$parent;
- } while (component);
+ } while (!stop && component);
};
});
diff --git a/src/plugins/utils.js b/src/plugins/utils.js
index 740c2a4..f2f2fed 100644
--- a/src/plugins/utils.js
+++ b/src/plugins/utils.js
@@ -3,6 +3,8 @@ import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
import dataUriToBuffer from "data-uri-to-buffer";
import ImageResize from "image-resize";
+export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
+
const sizeOf = require("image-size");
var dayjs = require('dayjs');
@@ -424,6 +426,36 @@ class Util {
});
}
+ /**
+ * Return 'true' if we should use voice mode for the given room.
+ *
+ * The default value is given by the room itself. If the "type" of the
+ * room is set to 'im.keanu.room_type_voice' then we default to voice mode,
+ * else not. The user can then override this default by flipping the "voice mode"
+ * swicth on room settings (it will be persisted as a user specific tag on the room)
+ */
+ useVoiceMode(roomOrNull) {
+ if (roomOrNull) {
+ const room = roomOrNull;
+
+ // Have we changed our local view mode of this room?
+ const tags = room.tags;
+ if (tags && tags["ui_options"]) {
+ return tags["ui_options"]["voice_mode"] === 1;
+ }
+
+ // Was the room created with a voice mode type?
+ const createEvent = room.currentState.getStateEvents(
+ "m.room.create",
+ ""
+ );
+ if (createEvent) {
+ return createEvent.getContent().type === ROOM_TYPE_VOICE_MODE;
+ }
+ }
+ return false;
+ }
+
/** Generate a random user name */
randomUser(prefix) {
var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, '') : null;
@@ -472,16 +504,22 @@ class Util {
return null;
}
- getFirstVisibleElement(parentNode) {
- const visible = this.findVisibleElements(parentNode);
+ getFirstVisibleElement(parentNode, where) {
+ let visible = this.findVisibleElements(parentNode);
+ if (visible) {
+ visible = visible.filter(where);
+ }
if (visible && visible.length > 0) {
return visible[0];
}
return null;
}
- getLastVisibleElement(parentNode) {
- const visible = this.findVisibleElements(parentNode);
+ getLastVisibleElement(parentNode, where) {
+ let visible = this.findVisibleElements(parentNode);
+ if (visible) {
+ visible = visible.filter(where);
+ }
if (visible && visible.length > 0) {
return visible[visible.length - 1];
}
diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js
index 2614652..2f802b3 100644
--- a/src/plugins/vuetify.js
+++ b/src/plugins/vuetify.js
@@ -1,37 +1,25 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
-import icUser from '@/assets/icons/user.vue';
-import icPassword from '@/assets/icons/password.vue';
-import icEdit from '@/assets/icons/edit.vue';
-import icGlobe from '@/assets/icons/globe.vue';
-import icAddReaction from '@/assets/icons/addReaction.vue';
-import icPoll from '@/assets/icons/poll.vue';
+
+// Import all .vue icons and process them, so they can be used
+// as $vuetify.icons.
+var icons = {}
+function importAll(r) {
+ return r.keys().map(res => {
+ // Remove"./"
+ const parts = res.split("/");
+ const iconName = parts[1].split(".")[0];
+ icons[iconName] = { component: r(res).default };
+ });
+}
+importAll(require.context('@/assets/icons/', true, /\.vue$/));
+
Vue.use(Vuetify);
export default new Vuetify({
icons: {
iconfont: 'md',
- values: {
- user: {
- component: icUser
- },
- password: {
- component: icPassword
- },
- edit: {
- component: icEdit
- },
- globe: {
- component: icGlobe
- },
- addReaction: {
- component: icAddReaction
- },
- poll: {
- component: icPoll
- },
- },
- user: icUser
+ values: icons,
},
});
diff --git a/src/services/config.service.js b/src/services/config.service.js
index 5e9cffb..7227a38 100644
--- a/src/services/config.service.js
+++ b/src/services/config.service.js
@@ -1,5 +1,5 @@
export default {
- install(Vue) {
+ install(Vue, defaultServerFromLocation) {
var config = Vue.observable(require('@/assets/config.json'));
const getRuntimeConfig = async () => {
const runtimeConfig = await fetch('./config.json');
@@ -14,7 +14,7 @@ export default {
}
// If default server is not set, default to current server address
if (!json.defaultServer) {
- Vue.set(config, "defaultServer", window.location.origin);
+ Vue.set(config, "defaultServer", defaultServerFromLocation);
}
});
Vue.prototype.$config = config;
diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js
index 158c971..7411211 100644
--- a/src/services/matrix.service.js
+++ b/src/services/matrix.service.js
@@ -5,8 +5,8 @@ import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
import util from "../plugins/utils";
import User from "../models/user";
-const LocalStorageCryptoStore = require("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store")
- .LocalStorageCryptoStore;
+const LocalStorageCryptoStore =
+ require("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store").LocalStorageCryptoStore;
export default {
install(Vue, options) {
@@ -37,6 +37,7 @@ export default {
userDisplayName: null,
userAvatar: null,
currentRoom: null,
+ currentRoomBeingPurged: false,
notificationCount: 0,
};
},
@@ -67,7 +68,7 @@ export default {
},
currentUserHomeServer() {
- return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId);
+ return this.$config.homeServer ? this.$config.homeServer : User.serverName(this.currentUserId);
},
currentRoomId() {
@@ -141,18 +142,16 @@ export default {
if (user.device_id) {
data.device_id = user.device_id;
}
- promiseLogin = tempMatrixClient
- .login("m.login.password", data)
- .then((response) => {
- var u = Object.assign({}, response);
- if (user.is_guest) {
- // 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;
- });
+ promiseLogin = tempMatrixClient.login("m.login.password", data).then((response) => {
+ var u = Object.assign({}, response);
+ if (user.is_guest) {
+ // 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;
+ });
}
return promiseLogin.then((user) => {
@@ -261,11 +260,7 @@ export default {
return matrixClient;
} else {
return new Promise((resolve, reject) => {
- matrixClient.once("sync", function(
- state,
- ignoredprevState,
- ignoredres
- ) {
+ matrixClient.once("sync", function (state, ignoredprevState, ignoredres) {
console.log(state); // state will be 'PREPARED' when the client is ready to use
if (state == "PREPARED") {
resolve(matrixClient);
@@ -293,11 +288,7 @@ export default {
if (this.ready) {
return Promise.resolve(this.currentUser);
}
- return this.$store.dispatch(
- "login",
- this.currentUser ||
- new User(this.$config.defaultServer, "", "", true)
- );
+ return this.$store.dispatch("login", this.currentUser || new User(this.$config.defaultServer, "", "", true));
},
addMatrixClientListeners(client) {
@@ -337,13 +328,7 @@ export default {
Vue.set(
room,
"avatar",
- room.getAvatarUrl(
- this.matrixClient.getHomeserverUrl(),
- 80,
- 80,
- "scale",
- true
- )
+ room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true)
);
}
}
@@ -351,24 +336,21 @@ export default {
case "m.room.member":
{
- 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!
if (
- event.getContent().membership == "leave" &&
- (event.getPrevContent() || {}).membership == "join" &&
- event.getStateKey() == this.currentUserId &&
- event.getSender() != this.currentUserId
+ (event.getContent().membership == "leave" &&
+ (event.getPrevContent() || {}).membership == "join" &&
+ event.getStateKey() == this.currentUserId &&
+ event.getSender() != this.currentUserId) ||
+ (event.getContent().membership == "ban" && event.getStateKey() == this.currentUserId)
) {
- // We were kicked
- const wasPurged =
- event.getContent().reason == "Room Deleted";
- this.$navigation.push(
- { name: "Goodbye", params: { roomWasPurged: wasPurged } },
- -1
- );
+ // We were kicked or banned
+ // If this is a live event (not just backpaging) then redirect to goodbye!
+ if (this.matrixClientReady) {
+ const wasPurged = event.getContent().reason == "Room Deleted";
+ this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
+ }
}
}
}
@@ -411,15 +393,15 @@ export default {
this.$store.commit("setUser", user);
// Login again
- this.login(user).catch(error => {
- if (error.data.errcode ==='M_FORBIDDEN' && this.currentUser.is_guest) {
+ this.login(user).catch((error) => {
+ if (error.data.errcode === "M_FORBIDDEN" && this.currentUser.is_guest) {
// Guest account and password don't work. We are in a strange state, probably because
// of server cleanup of accounts or similar. Wipe account and restart...
this.$store.commit("setUser", null);
}
this.$store.commit("setCurrentRoomId", null);
this.$navigation.push({ path: "/login" }, -1);
- })
+ });
} else {
this.$store.commit("setUser", null);
this.$store.commit("setCurrentRoomId", null);
@@ -432,24 +414,11 @@ export default {
// each time!
var updatedRooms = this.matrixClient.getVisibleRooms();
updatedRooms = updatedRooms.filter((room) => {
- return (
- room.selfMembership &&
- (room.selfMembership == "invite" || room.selfMembership == "join")
- );
+ return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join");
});
updatedRooms.forEach((room) => {
if (!room.avatar) {
- Vue.set(
- room,
- "avatar",
- room.getAvatarUrl(
- this.matrixClient.getHomeserverUrl(),
- 80,
- 80,
- "scale",
- true
- )
- );
+ Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
}
});
Vue.set(this, "rooms", updatedRooms);
@@ -488,15 +457,9 @@ export default {
var ids = {};
const ret = [];
for (const room of this.rooms) {
- if (
- room.selfMembership == "join" &&
- this.getRoomJoinRule(room) == "invite"
- ) {
+ if (room.selfMembership == "join" && this.getRoomJoinRule(room) == "invite") {
for (const member of room.getJoinedMembers()) {
- if (
- member.userId != this.currentUserId &&
- !ids[member.userId]
- ) {
+ if (member.userId != this.currentUserId && !ids[member.userId]) {
ids[member.userId] = member;
ret.push(member);
}
@@ -513,10 +476,7 @@ export default {
getRoomJoinRule(room) {
if (room) {
- const joinRules = room.currentState.getStateEvents(
- "m.room.join_rules",
- ""
- );
+ const joinRules = room.currentState.getStateEvents("m.room.join_rules", "");
return joinRules && joinRules.getContent().join_rule;
}
return null;
@@ -524,14 +484,8 @@ export default {
getRoomHistoryVisibility(room) {
if (room) {
- const historyVisibility = room.currentState.getStateEvents(
- "m.room.history_visibility",
- ""
- );
- return (
- historyVisibility &&
- historyVisibility.getContent().history_visibility
- );
+ const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
+ return historyVisibility && historyVisibility.getContent().history_visibility;
}
return null;
},
@@ -546,6 +500,55 @@ export default {
});
},
+ 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);
+ }
+ }
+ }
+ },
+
/**
* Purge the room with the given id! This means:
* - Make room invite only
@@ -556,6 +559,28 @@ export default {
* @param roomId
*/
purgeRoom(roomId, statusCallback) {
+ this.currentRoomBeingPurged = true;
+
+ 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);
@@ -564,22 +589,13 @@ export default {
return;
}
- const timelineWindow = new TimelineWindow(
- this.matrixClient,
- room.getUnfilteredTimelineSet(),
- {}
- );
+ 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"));
this.matrixClient
- .sendStateEvent(
- roomId,
- "m.room.join_rules",
- { join_rule: "invite" },
- ""
- )
+ .sendStateEvent(roomId, "m.room.join_rules", { join_rule: "invite" }, "")
.then(() => {
//console.log("Purge: forbid guest access");
return this.matrixClient.sendStateEvent(
@@ -591,13 +607,9 @@ export default {
})
.then(() => {
//console.log("Purge: set history visibility to 'joined'");
- return this.matrixClient.sendStateEvent(
- roomId,
- "m.room.history_visibility",
- {
- history_visibility: "joined",
- }
- );
+ return this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", {
+ history_visibility: "joined",
+ });
})
.then(() => {
//console.log("Purge: create timeline");
@@ -607,11 +619,12 @@ 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((ignoredsuccess) => {
+ return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((gotmore) => {
+ if (gotmore) {
return _getMoreIfAvailable.call(self);
- });
+ }
+ return Promise.resolve("Done");
+ });
} else {
return Promise.resolve("Done");
}
@@ -623,60 +636,77 @@ export default {
statusCallback(this.$t("room.purge_redacting_events"));
// First ignore unknown device errors
this.matrixClient.setGlobalErrorOnUnknownDevices(false);
- var redactionPromises = [];
- timelineWindow.getEvents().forEach((event) => {
- if (
+ const allEvents = timelineWindow.getEvents().filter((event) => {
+ return (
!event.isRedacted() &&
!event.isRedaction() &&
- !event.isState()
- ) {
- // Redact!
- 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(() => {
//console.log("Purge: kick members");
statusCallback(this.$t("room.purge_removing_members"));
var joined = room.getMembersWithMembership("join");
var invited = room.getMembersWithMembership("invite");
- var members = joined.concat(invited);
+ var allMembers = joined.concat(invited);
- var kickPromises = [];
- members.forEach((member) => {
- if (member.userId != self.currentUserId) {
- kickPromises.push(
- this.matrixClient.kick(
- roomId,
- member.userId,
- "Room Deleted"
- )
- );
+ 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);
}
- });
- return Promise.all(kickPromises);
+ 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(() => this.matrixClient.kick(roomId, member.userId, "Room Deleted")))
+ .then(() => kickFirstMember(members.slice(1)));
+ }
+ };
+
+ return kickFirstMember(allMembers);
})
.then(() => {
statusCallback(null);
- this.matrixClient.setGlobalErrorOnUnknownDevices(
- oldGlobalErrorSetting
- );
- return this.leaveRoom(roomId);
+ this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
+ return withRetry(() => this.leaveRoom(roomId));
})
.then(() => {
+ this.currentRoomBeingPurged = false;
resolve(true); // Done!
})
.catch((err) => {
console.error("Error purging room", err);
- this.matrixClient.setGlobalErrorOnUnknownDevices(
- oldGlobalErrorSetting
- );
+ this.currentRoomBeingPurged = false;
+ this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
reject(err);
});
});
@@ -754,10 +784,7 @@ export default {
* @param {*} userId
*/
isDirectRoomWith(room, userId) {
- if (
- 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") {
@@ -829,9 +856,7 @@ export default {
return this.matrixClient;
});
} else {
- const tempMatrixClient = sdk.createClient(
- this.$config.defaultServer
- );
+ const tempMatrixClient = sdk.createClient(this.$config.defaultServer);
var tempUserString = this.$store.state.tempuser;
var tempUser = null;
if (tempUserString) {
@@ -911,13 +936,7 @@ export default {
})
.then((response) => {
if (response.avatar_url) {
- response.avatar = matrixClient.mxcUrlToHttp(
- response.avatar_url,
- 80,
- 80,
- "scale",
- true
- );
+ response.avatar = matrixClient.mxcUrlToHttp(response.avatar_url, 80, 80, "scale", true);
}
return Promise.resolve(response);
})
@@ -946,13 +965,7 @@ export default {
(roomId.startsWith("!") && room.room_id == roomId)
) {
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", true);
}
return Promise.resolve(room);
}
@@ -999,9 +1012,7 @@ export default {
},
});
- sdk.setCryptoStoreFactory(
- matrixService.createCryptoStore.bind(matrixService)
- );
+ sdk.setCryptoStoreFactory(matrixService.createCryptoStore.bind(matrixService));
Vue.prototype.$matrix = matrixService;
},