{{ notificationCount(room) }}
diff --git a/src/components/VoiceRecorder.vue b/src/components/VoiceRecorder.vue
index acd4251..d1debf6 100644
--- a/src/components/VoiceRecorder.vue
+++ b/src/components/VoiceRecorder.vue
@@ -233,6 +233,7 @@ export default {
this.state = State.INITIAL;
this.errorMessage = null;
this.recordedFile = null;
+ this.recordingTime = String.fromCharCode(160);
if (this.ptt) {
document.addEventListener("mouseup", this.mouseUp, false);
document.addEventListener("mousemove", this.mouseMove, false);
diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js
new file mode 100644
index 0000000..c8d5356
--- /dev/null
+++ b/src/components/chatMixin.js
@@ -0,0 +1,247 @@
+import util from "../plugins/utils";
+import MessageIncomingText from "./messages/MessageIncomingText";
+import MessageIncomingFile from "./messages/MessageIncomingFile";
+import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
+import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
+import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
+import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
+import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue";
+import MessageOutgoingText from "./messages/MessageOutgoingText";
+import MessageOutgoingFile from "./messages/MessageOutgoingFile";
+import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
+import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
+import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
+import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
+import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
+import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
+import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
+import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
+import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
+import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
+import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
+import ContactJoin from "./messages/ContactJoin.vue";
+import ContactLeave from "./messages/ContactLeave.vue";
+import ContactInvited from "./messages/ContactInvited.vue";
+import ContactChanged from "./messages/ContactChanged.vue";
+import RoomCreated from "./messages/RoomCreated.vue";
+import RoomAliased from "./messages/RoomAliased.vue";
+import RoomNameChanged from "./messages/RoomNameChanged.vue";
+import RoomTopicChanged from "./messages/RoomTopicChanged.vue";
+import RoomAvatarChanged from "./messages/RoomAvatarChanged.vue";
+import RoomHistoryVisibility from "./messages/RoomHistoryVisibility.vue";
+import MessageOperations from "./messages/MessageOperations.vue";
+import AvatarOperations from "./messages/AvatarOperations.vue";
+import ChatHeader from "./ChatHeader";
+import VoiceRecorder from "./VoiceRecorder";
+import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
+import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
+import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
+import stickers from "../plugins/stickers";
+import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
+import BottomSheet from "./BottomSheet.vue";
+import CreatePollDialog from "./CreatePollDialog.vue";
+import RoomJoinRules from "./messages/RoomJoinRules.vue";
+import RoomPowerLevelsChanged from "./messages/RoomPowerLevelsChanged.vue";
+import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
+import RoomEncrypted from "./messages/RoomEncrypted.vue";
+import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
+import DebugEvent from "./messages/DebugEvent.vue";
+
+export default {
+ components: {
+ ChatHeader,
+ MessageIncomingText,
+ MessageIncomingFile,
+ MessageIncomingImage,
+ MessageIncomingAudio,
+ MessageIncomingVideo,
+ MessageIncomingSticker,
+ MessageOutgoingText,
+ MessageOutgoingFile,
+ MessageOutgoingImage,
+ MessageOutgoingAudio,
+ MessageOutgoingVideo,
+ MessageOutgoingSticker,
+ MessageOutgoingPoll,
+ ContactJoin,
+ ContactLeave,
+ ContactInvited,
+ ContactChanged,
+ RoomCreated,
+ RoomAliased,
+ RoomNameChanged,
+ RoomTopicChanged,
+ RoomAvatarChanged,
+ RoomHistoryVisibility,
+ RoomJoinRules,
+ RoomPowerLevelsChanged,
+ RoomGuestAccessChanged,
+ RoomEncrypted,
+ RoomDeletionNotice,
+ DebugEvent,
+ MessageOperations,
+ VoiceRecorder,
+ RoomInfoBottomSheet,
+ CreatedRoomWelcomeHeader,
+ MessageOperationsBottomSheet,
+ StickerPickerBottomSheet,
+ BottomSheet,
+ AvatarOperations,
+ CreatePollDialog,
+ },
+ methods: {
+ showDayMarkerBeforeEvent(event) {
+ const idx = this.events.indexOf(event);
+ if (idx <= 0) {
+ return true;
+ }
+ const previousEvent = this.events[idx - 1];
+ return util.dayDiff(previousEvent.getTs(), event.getTs()) > 0;
+ },
+
+ dayForEvent(event) {
+ let dayDiff = util.dayDiffToday(event.getTs());
+ if (dayDiff < 7) {
+ return this.$tc("message.time_ago", dayDiff);
+ } else {
+ return util.formatDay(event.getTs());
+ }
+ },
+
+ dateForEvent(event) {
+ return util.formatDay(event.getTs());
+ },
+
+ componentForEvent(event, isForExport = false) {
+ switch (event.getType()) {
+ case "m.room.member":
+ if (event.getContent().membership == "join") {
+ if (event.getPrevContent() && event.getPrevContent().membership == "join") {
+ // We we already joined, so this must be a display name and/or avatar update!
+ return ContactChanged;
+ } else {
+ return ContactJoin;
+ }
+ } else if (event.getContent().membership == "leave") {
+ return ContactLeave;
+ } else if (event.getContent().membership == "invite") {
+ return ContactInvited;
+ }
+ break;
+
+ case "m.room.message":
+ if (event.getSender() != this.$matrix.currentUserId) {
+ if (event.getContent().msgtype == "m.image") {
+ // For SVG, make downloadable
+ if (
+ event.getContent().info &&
+ event.getContent().info.mimetype &&
+ event.getContent().info.mimetype.startsWith("image/svg")
+ ) {
+ return MessageIncomingFile;
+ }
+ if (isForExport) {
+ return MessageIncomingImageExport;
+ }
+ return MessageIncomingImage;
+ } else if (event.getContent().msgtype == "m.audio") {
+ if (isForExport) {
+ return MessageIncomingAudioExport;
+ }
+ return MessageIncomingAudio;
+ } else if (event.getContent().msgtype == "m.video") {
+ if (isForExport) {
+ return MessageIncomingVideoExport;
+ }
+ return MessageIncomingVideo;
+ } else if (event.getContent().msgtype == "m.file") {
+ return MessageIncomingFile;
+ } else if (stickers.isStickerShortcode(event.getContent().body)) {
+ return MessageIncomingSticker;
+ }
+ return MessageIncomingText;
+ } else {
+ if (event.getContent().msgtype == "m.image") {
+ // For SVG, make downloadable
+ if (
+ event.getContent().info &&
+ event.getContent().info.mimetype &&
+ event.getContent().info.mimetype.startsWith("image/svg")
+ ) {
+ return MessageOutgoingImage;
+ }
+ if (isForExport) {
+ return MessageOutgoingImageExport;
+ }
+ return MessageOutgoingImage;
+ } else if (event.getContent().msgtype == "m.audio") {
+ if (isForExport) {
+ return MessageOutgoingAudioExport;
+ }
+ return MessageOutgoingAudio;
+ } else if (event.getContent().msgtype == "m.video") {
+ if (isForExport) {
+ return MessageOutgoingVideoExport;
+ }
+ return MessageOutgoingVideo;
+ } else if (event.getContent().msgtype == "m.file") {
+ return MessageOutgoingFile;
+ } else if (stickers.isStickerShortcode(event.getContent().body)) {
+ return MessageOutgoingSticker;
+ }
+ return MessageOutgoingText;
+ }
+
+ case "m.room.create":
+ return RoomCreated;
+
+ case "m.room.canonical_alias":
+ return RoomAliased;
+
+ case "m.room.name":
+ return RoomNameChanged;
+
+ case "m.room.topic":
+ return RoomTopicChanged;
+
+ case "m.room.avatar":
+ return RoomAvatarChanged;
+
+ case "m.room.history_visibility":
+ return RoomHistoryVisibility;
+
+ case "m.room.join_rules":
+ return RoomJoinRules;
+
+ case "m.room.power_levels":
+ return RoomPowerLevelsChanged;
+
+ case "m.room.guest_access":
+ return RoomGuestAccessChanged;
+
+ case "m.room.encryption":
+ return RoomEncrypted;
+
+ case "m.poll.start":
+ case "org.matrix.msc3381.poll.start":
+ if (event.getSender() != this.$matrix.currentUserId) {
+ return MessageIncomingPoll;
+ } else {
+ return MessageOutgoingPoll;
+ }
+
+ case "im.keanu.room_deletion_notice": {
+ // Custom event for notice 30 seconds before a room is deleted/purged.
+ const deletionNotices = this.room.currentState.getStateEvents("im.keanu.room_deletion_notice");
+ if (deletionNotices && deletionNotices.length > 0 && deletionNotices[deletionNotices.length - 1] == event) {
+ // This is the latest/last one. Look at the status flag. Show nothing if it is "cancel".
+ if (event.getContent().status != "cancel") {
+ return RoomDeletionNotice;
+ }
+ }
+ }
+ }
+ return this.debugging ? DebugEvent : null;
+ },
+ },
+};
diff --git a/src/components/messages/MessageIncomingPoll.vue b/src/components/messages/MessageIncomingPoll.vue
new file mode 100644
index 0000000..2b956a3
--- /dev/null
+++ b/src/components/messages/MessageIncomingPoll.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
{{ pollQuestion }}
+
+
+ {{ answer.text }} {{ answer.max }}
+
+ {{ answer.percentage }}%
+
+
+
+
+ {{ $t("poll_create.num_answered", { count: pollNumAnswers }) }}
+
+
+ {{ $t("poll_create.close_poll") }}
+
+
+
+
+ {{ $t("poll_create.poll_submit") }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/messages/MessageOperations.vue b/src/components/messages/MessageOperations.vue
index 75429a2..20d53a6 100644
--- a/src/components/messages/MessageOperations.vue
+++ b/src/components/messages/MessageOperations.vue
@@ -1,16 +1,26 @@
-
+
{{ item.data }}
-
+
+ $vuetify.icons.addReaction
+
+
reply
-
- more_horiz
-
+
+ edit
+
+
+ delete
+
+
+ get_app
+
+
+
+
diff --git a/src/components/messages/export/MessageIncomingAudioExport.vue b/src/components/messages/export/MessageIncomingAudioExport.vue
new file mode 100644
index 0000000..e9b2233
--- /dev/null
+++ b/src/components/messages/export/MessageIncomingAudioExport.vue
@@ -0,0 +1,21 @@
+