Export chat (from room info page)
This commit is contained in:
parent
dd48bedfb5
commit
95555a23e4
16 changed files with 18905 additions and 657 deletions
18444
package-lock.json
generated
18444
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -16,12 +16,14 @@
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"data-uri-to-buffer": "^3.0.1",
|
"data-uri-to-buffer": "^3.0.1",
|
||||||
"dayjs": "^1.10.3",
|
"dayjs": "^1.10.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"fix-webm-duration": "^1.0.0",
|
"fix-webm-duration": "^1.0.0",
|
||||||
"image-resize": "^1.1.5",
|
"image-resize": "^1.1.5",
|
||||||
"image-size": "^1.0.0",
|
"image-size": "^1.0.0",
|
||||||
"intersection-observer": "^0.12",
|
"intersection-observer": "^0.12",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"json-web-key": "^0.4.0",
|
"json-web-key": "^0.4.0",
|
||||||
|
"jszip": "^3.9.1",
|
||||||
"linkifyjs": "3.0.0-beta.3",
|
"linkifyjs": "3.0.0-beta.3",
|
||||||
"material-design-icons-iconfont": "^6.1",
|
"material-design-icons-iconfont": "^6.1",
|
||||||
"matrix-js-sdk": "^15.2.0",
|
"matrix-js-sdk": "^15.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1137,4 +1137,9 @@ $admin-fg: white;
|
||||||
.loading-indicator {
|
.loading-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exporting-indicator {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +210,8 @@
|
||||||
"show_all": "Show all >",
|
"show_all": "Show all >",
|
||||||
"leave_room": "Leave",
|
"leave_room": "Leave",
|
||||||
"version_info": "Powered by Guardian Project. Version: {version}",
|
"version_info": "Powered by Guardian Project. Version: {version}",
|
||||||
"scan_code": "Scan to join the room"
|
"scan_code": "Scan to join the room",
|
||||||
|
"export_room": "Export chat"
|
||||||
},
|
},
|
||||||
"room_info_sheet": {
|
"room_info_sheet": {
|
||||||
"this_room": "This room",
|
"this_room": "This room",
|
||||||
|
|
@ -257,5 +258,11 @@
|
||||||
"close_poll": "Close poll",
|
"close_poll": "Close poll",
|
||||||
"poll_submit": "Submit",
|
"poll_submit": "Submit",
|
||||||
"num_answered": "{count} have answered"
|
"num_answered": "{count} have answered"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"exported_date": "Exported on {date}",
|
||||||
|
"fetched_n_events": "Fetched {count} events",
|
||||||
|
"fetched_n_of_total_events": "Fetched {count} of {total} events",
|
||||||
|
"processed_n_of_total_events": "Processed media for {count} of {total} events"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -384,36 +384,6 @@
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
||||||
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 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 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";
|
|
||||||
import util from "../plugins/utils";
|
import util from "../plugins/utils";
|
||||||
import MessageOperations from "./messages/MessageOperations.vue";
|
import MessageOperations from "./messages/MessageOperations.vue";
|
||||||
import AvatarOperations from "./messages/AvatarOperations.vue";
|
import AvatarOperations from "./messages/AvatarOperations.vue";
|
||||||
|
|
@ -422,11 +392,11 @@ import VoiceRecorder from "./VoiceRecorder";
|
||||||
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
|
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
|
||||||
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
|
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
|
||||||
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
|
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
|
||||||
import stickers from "../plugins/stickers";
|
|
||||||
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
|
||||||
import BottomSheet from "./BottomSheet.vue";
|
import BottomSheet from "./BottomSheet.vue";
|
||||||
import ImageResize from "image-resize";
|
import ImageResize from "image-resize";
|
||||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||||
|
import chatMixin from "./chatMixin";
|
||||||
|
|
||||||
const sizeOf = require("image-size");
|
const sizeOf = require("image-size");
|
||||||
const dataUriToBuffer = require("data-uri-to-buffer");
|
const dataUriToBuffer = require("data-uri-to-buffer");
|
||||||
|
|
@ -462,37 +432,9 @@ ScrollPosition.prototype.prepareFor = function(direction) {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Chat",
|
name: "Chat",
|
||||||
|
mixins: [chatMixin],
|
||||||
components: {
|
components: {
|
||||||
ChatHeader,
|
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,
|
MessageOperations,
|
||||||
VoiceRecorder,
|
VoiceRecorder,
|
||||||
RoomInfoBottomSheet,
|
RoomInfoBottomSheet,
|
||||||
|
|
@ -934,120 +876,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentForEvent(event) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return MessageIncomingImage;
|
|
||||||
} else if (event.getContent().msgtype == "m.audio") {
|
|
||||||
return MessageIncomingAudio;
|
|
||||||
} else if (event.getContent().msgtype == "m.video") {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return MessageOutgoingImage;
|
|
||||||
} else if (event.getContent().msgtype == "m.audio") {
|
|
||||||
return MessageOutgoingAudio;
|
|
||||||
} else if (event.getContent().msgtype == "m.video") {
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
paginateBackIfNeeded() {
|
paginateBackIfNeeded() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const container = this.$refs.chatContainer;
|
const container = this.$refs.chatContainer;
|
||||||
|
|
@ -1589,24 +1417,6 @@ export default {
|
||||||
this.restartRRTimer();
|
this.restartRRTimer();
|
||||||
},
|
},
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showRecordingUI() {
|
showRecordingUI() {
|
||||||
this.showRecorderPTT = false;
|
this.showRecorderPTT = false;
|
||||||
this.showRecorder = true;
|
this.showRecorder = true;
|
||||||
|
|
|
||||||
428
src/components/RoomExport.vue
Normal file
428
src/components/RoomExport.vue
Normal file
|
|
@ -0,0 +1,428 @@
|
||||||
|
<template>
|
||||||
|
<div class="chat-root">
|
||||||
|
<div class="chat-root d-flex flex-column" ref="exportRoot">
|
||||||
|
<!-- Header-->
|
||||||
|
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
|
||||||
|
<v-row class="chat-header-row flex-nowrap">
|
||||||
|
<v-col cols="auto" class="chat-header-members text-start ma-0 pa-0" @click.stop="onHeaderClicked">
|
||||||
|
<v-avatar size="40" class="me-2">
|
||||||
|
<v-img v-if="room.avatar || memberAvatar" :src="room.avatar || memberAvatar" />
|
||||||
|
</v-avatar>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-nowrap">
|
||||||
|
<div class="room-name-inline text-truncate" :title="room.name">
|
||||||
|
{{ room.name }}
|
||||||
|
</div>
|
||||||
|
<div class="num-members">{{ $tc("room.members", room.getJoinedMemberCount()) }}</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto" class="text-end ma-0 pa-0">{{ exportDate }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
|
||||||
|
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
|
||||||
|
<!-- DAY Marker, shown for every new day in the timeline -->
|
||||||
|
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker" :title="dateForEvent(event)" />
|
||||||
|
|
||||||
|
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
||||||
|
<div class="message-wrapper">
|
||||||
|
<component
|
||||||
|
:is="componentForEvent(event, true)"
|
||||||
|
:room="room"
|
||||||
|
:event="event"
|
||||||
|
:nextEvent="events[index + 1]"
|
||||||
|
:reactions="timelineSet.getRelationsForEvent(event.getId(), 'm.annotation', 'm.reaction')"
|
||||||
|
:timelineSet="timelineSet"
|
||||||
|
ref="exportedEvent"
|
||||||
|
/>
|
||||||
|
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
||||||
|
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<v-container fluid fill-height class="exporting-indicator">
|
||||||
|
<v-row align="center" justify="center">
|
||||||
|
<v-col class="text-center">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
<div>{{ statusText }}</div>
|
||||||
|
<v-btn color="black" depressed class="filled-button mt-5" @click.stop="cancelExport">{{
|
||||||
|
$t("menu.cancel")
|
||||||
|
}}</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageIncomingText from "./messages/MessageIncomingText.vue";
|
||||||
|
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
|
||||||
|
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 MessageOutgoingText from "./messages/MessageOutgoingText.vue";
|
||||||
|
import MessageOutgoingFile from "./messages/MessageOutgoingFile.vue";
|
||||||
|
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 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 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";
|
||||||
|
import MessageOperations from "./messages/MessageOperations.vue";
|
||||||
|
import AvatarOperations from "./messages/AvatarOperations.vue";
|
||||||
|
import ChatHeader from "./ChatHeader.vue";
|
||||||
|
import VoiceRecorder from "./VoiceRecorder.vue";
|
||||||
|
import RoomInfoBottomSheet from "./RoomInfoBottomSheet.vue";
|
||||||
|
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader.vue";
|
||||||
|
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet.vue";
|
||||||
|
import StickerPickerBottomSheet from "./StickerPickerBottomSheet.vue";
|
||||||
|
import BottomSheet from "./BottomSheet.vue";
|
||||||
|
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||||
|
import chatMixin from "./chatMixin";
|
||||||
|
import util from "../plugins/utils";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "RoomExport",
|
||||||
|
mixins: [chatMixin],
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
room: {
|
||||||
|
type: Object,
|
||||||
|
default: function() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
fetchedEvents: 0,
|
||||||
|
totalEvents: 0,
|
||||||
|
processedEvents: 0,
|
||||||
|
statusText: "",
|
||||||
|
cancelled: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.doExport();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
processedEvents() {
|
||||||
|
this.statusText = this.$t("export.processed_n_of_total_events", {
|
||||||
|
count: this.processedEvents,
|
||||||
|
total: this.totalEvents,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
exportDate() {
|
||||||
|
return this.$t("export.exported_date", {date: util.formatDay(Date.now().valueOf())});
|
||||||
|
},
|
||||||
|
timelineSet() {
|
||||||
|
return this.room.getUnfilteredTimelineSet();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelExport() {
|
||||||
|
this.cancelled = true;
|
||||||
|
},
|
||||||
|
async getEvents() {
|
||||||
|
const eventsPerBatch = 100;
|
||||||
|
let batchToken = null;
|
||||||
|
var nToFetch = null;
|
||||||
|
this.totalEvents = nToFetch == null ? 0 : nToFetch;
|
||||||
|
var fetchedEvents = [];
|
||||||
|
const eventMapper = this.$matrix.matrixClient.getEventMapper();
|
||||||
|
|
||||||
|
while (nToFetch == null || nToFetch > 0) {
|
||||||
|
const result = await this.$matrix.matrixClient.createMessagesRequest(
|
||||||
|
this.room.roomId,
|
||||||
|
batchToken,
|
||||||
|
nToFetch == null ? eventsPerBatch : Math.min(nToFetch, eventsPerBatch),
|
||||||
|
"b"
|
||||||
|
);
|
||||||
|
// For testing, uncomment to give a chance to cancel...
|
||||||
|
// await new Promise((resolve, ignoredReject) => {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// resolve(true);
|
||||||
|
// }, 1000);
|
||||||
|
// });
|
||||||
|
if (this.cancelled) {
|
||||||
|
return Promise.reject("cancelled");
|
||||||
|
}
|
||||||
|
if (result.chunk.length === 0) break;
|
||||||
|
if (nToFetch != null) {
|
||||||
|
nToFetch -= result.chunk.length;
|
||||||
|
this.statusText = this.$t("export.fetched_n_of_total_events", {
|
||||||
|
count: this.totalEvents - nToFetch,
|
||||||
|
total: this.totalEvents,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.totalEvents += result.chunk.length;
|
||||||
|
this.statusText = this.$t("export.fetched_n_events", { count: this.totalEvents });
|
||||||
|
}
|
||||||
|
fetchedEvents.push(...result.chunk.map(eventMapper));
|
||||||
|
|
||||||
|
if (!result.end) break;
|
||||||
|
batchToken = result.end;
|
||||||
|
}
|
||||||
|
return fetchedEvents;
|
||||||
|
},
|
||||||
|
doExport() {
|
||||||
|
var zip = null;
|
||||||
|
var currentMediaSize = 0;
|
||||||
|
var maxMediaSize = 1024 * 1024 * 1024; // 1GB
|
||||||
|
|
||||||
|
this.getEvents()
|
||||||
|
.then((events) => {
|
||||||
|
this.events = events.reverse();
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// UI updated, start processing events
|
||||||
|
zip = new JSZip();
|
||||||
|
var imageFolder = zip.folder("images");
|
||||||
|
var audioFolder = zip.folder("audio");
|
||||||
|
var videoFolder = zip.folder("video");
|
||||||
|
|
||||||
|
var downloadPromises = [];
|
||||||
|
let components = this.$refs.exportedEvent;
|
||||||
|
for (const comp of components) {
|
||||||
|
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
|
||||||
|
switch (componentClass) {
|
||||||
|
case "MessageIncomingImageExport":
|
||||||
|
case "MessageOutgoingImageExport":
|
||||||
|
// TODO - maybe consider what media to download based on the file size we already have?
|
||||||
|
// info = comp.event.getContent().info;
|
||||||
|
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
|
||||||
|
// // No need to even download.
|
||||||
|
// console.log("Dont download!");
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
let mime = blob.type;
|
||||||
|
var extension = ".png";
|
||||||
|
switch (mime) {
|
||||||
|
case "image/jpeg":
|
||||||
|
case "image/jpg":
|
||||||
|
extension = ".jpg";
|
||||||
|
break;
|
||||||
|
case "image/gif":
|
||||||
|
extension = ".gif";
|
||||||
|
}
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
|
imageFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
|
||||||
|
let blobUrl = URL.createObjectURL(blob);
|
||||||
|
comp.src = blobUrl;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// Update source
|
||||||
|
let elements = comp.$el.getElementsByClassName("v-image__image");
|
||||||
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.style.backgroundImage = 'url("./images/' + fileName + '")';
|
||||||
|
element.classList.remove("v-image__image--preload");
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(blobUrl); // Give the blob back
|
||||||
|
this.processedEvents += 1;
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "MessageIncomingAudioExport":
|
||||||
|
case "MessageOutgoingAudioExport":
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
//let mime = blob.type;
|
||||||
|
var extension = ".mp3";
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
|
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
let elements = comp.$el.getElementsByTagName("audio");
|
||||||
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.src = "./audio/" + fileName;
|
||||||
|
}
|
||||||
|
this.processedEvents += 1;
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "MessageIncomingVideoExport":
|
||||||
|
case "MessageOutgoingVideoExport":
|
||||||
|
downloadPromises.push(
|
||||||
|
util
|
||||||
|
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
|
||||||
|
.then((blob) => {
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
return new Promise((resolve, ignoredReject) => {
|
||||||
|
//let mime = blob.type;
|
||||||
|
var extension = ".mp4";
|
||||||
|
let fileName = comp.event.getId() + extension;
|
||||||
|
videoFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
let elements = comp.$el.getElementsByTagName("video");
|
||||||
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.src = "./video/" + fileName;
|
||||||
|
}
|
||||||
|
this.processedEvents += 1;
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.processedEvents += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(downloadPromises);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log("All media added, total size: " + currentMediaSize);
|
||||||
|
|
||||||
|
let root = this.$refs.exportRoot;
|
||||||
|
var doc = "<html><head>\n";
|
||||||
|
|
||||||
|
for (const sheet of document.styleSheets) {
|
||||||
|
doc += "<style type='text/css'>\n";
|
||||||
|
for (const rule of sheet.cssRules) {
|
||||||
|
if (rule.constructor.name != "CSSFontFaceRule") {
|
||||||
|
// Strip font face rules for now.
|
||||||
|
doc += rule.cssText + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc += "</style>\n";
|
||||||
|
}
|
||||||
|
doc += "</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||||
|
const getCssRules = function(el) {
|
||||||
|
if (el.classList.contains("op-button")) {
|
||||||
|
el.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < el.children.length; i++) {
|
||||||
|
getCssRules(el.children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getCssRules(root);
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
doc += this.$refs.exportRoot.outerHTML;
|
||||||
|
doc += "</div></body></html>";
|
||||||
|
|
||||||
|
zip.file("chat.html", doc);
|
||||||
|
zip.generateAsync({ type: "blob" }).then((content) => {
|
||||||
|
saveAs(content, "example.zip");
|
||||||
|
this.status = "";
|
||||||
|
this.$emit("close");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to export:", err);
|
||||||
|
this.$emit("close");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -167,6 +167,17 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- EXPORT CHAT -->
|
||||||
|
<div style="text-align: center">
|
||||||
|
<v-btn
|
||||||
|
v-if="userCanExportChat"
|
||||||
|
color="black"
|
||||||
|
depressed
|
||||||
|
class="filled-button"
|
||||||
|
@click.stop="exportRoom"
|
||||||
|
>{{ $t("room_info.export_room") }}</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- PURGE ROOM -->
|
<!-- PURGE ROOM -->
|
||||||
<div class="members ma-3 pa-3 text-center">
|
<div class="members ma-3 pa-3 text-center">
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -179,7 +190,7 @@
|
||||||
>{{ $t("room_info.purge") }}</v-btn
|
>{{ $t("room_info.purge") }}</v-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="build-version">
|
<div class="build-version">
|
||||||
{{ $t("room_info.version_info", { version: buildVersion }) }}
|
{{ $t("room_info.version_info", { version: buildVersion }) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -198,6 +209,7 @@
|
||||||
|
|
||||||
<QRCodePopup :show="showFullScreenQR" :message="publicRoomLink" @close="showFullScreenQR = false" />
|
<QRCodePopup :show="showFullScreenQR" :message="publicRoomLink" @close="showFullScreenQR = false" />
|
||||||
|
|
||||||
|
<RoomExport :room="room" v-if="exporting" v-on:close="exporting = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -205,6 +217,7 @@
|
||||||
import LeaveRoomDialog from "../components/LeaveRoomDialog";
|
import LeaveRoomDialog from "../components/LeaveRoomDialog";
|
||||||
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
import PurgeRoomDialog from "../components/PurgeRoomDialog";
|
||||||
import DeviceList from "../components/DeviceList";
|
import DeviceList from "../components/DeviceList";
|
||||||
|
import RoomExport from "../components/RoomExport";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import roomInfoMixin from "./roomInfoMixin";
|
import roomInfoMixin from "./roomInfoMixin";
|
||||||
import QRCodePopup from './QRCodePopup.vue';
|
import QRCodePopup from './QRCodePopup.vue';
|
||||||
|
|
@ -217,6 +230,7 @@ export default {
|
||||||
LeaveRoomDialog,
|
LeaveRoomDialog,
|
||||||
PurgeRoomDialog,
|
PurgeRoomDialog,
|
||||||
DeviceList,
|
DeviceList,
|
||||||
|
RoomExport,
|
||||||
QRCodePopup,
|
QRCodePopup,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -244,7 +258,8 @@ export default {
|
||||||
icon: "person_add",
|
icon: "person_add",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
SHOW_MEMBER_LIMIT: 5
|
SHOW_MEMBER_LIMIT: 5,
|
||||||
|
exporting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
@ -465,6 +480,11 @@ export default {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
exportRoom() {
|
||||||
|
if (this.room) {
|
||||||
|
this.exporting = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
247
src/components/chatMixin.js
Normal file
247
src/components/chatMixin.js
Normal file
|
|
@ -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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<audio controls :src="src">{{ $t("fallbacks.audio_file") }}</audio>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageIncomingAudioExport",
|
||||||
|
extends: MessageIncoming,
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
components: { MessageIncoming },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-img :aspect-ratio="16 / 9" ref="image" :src="src" :cover="cover" :contain="contain" />
|
||||||
|
</div>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageIncomingImageExport",
|
||||||
|
extends: MessageIncoming,
|
||||||
|
components: { MessageIncoming },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
src: null,
|
||||||
|
cover: true,
|
||||||
|
contain: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const info = this.event.getContent().info;
|
||||||
|
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
||||||
|
// be stickers and small emoji type things.
|
||||||
|
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
||||||
|
this.cover = true;
|
||||||
|
this.contain = false;
|
||||||
|
} else {
|
||||||
|
this.cover = false;
|
||||||
|
this.contain = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.src) {
|
||||||
|
const objectUrl = this.src;
|
||||||
|
this.src = null;
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-responsive :aspect-ratio="16 / 9" :src="src">
|
||||||
|
<video :src="src" controls class="w-100 h-100">
|
||||||
|
{{ $t("fallbacks.video_file") }}
|
||||||
|
</video>
|
||||||
|
</v-responsive>
|
||||||
|
</div>
|
||||||
|
</message-incoming>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageIncoming from "../MessageIncoming.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageIncomingVideoExport",
|
||||||
|
extends: MessageIncoming,
|
||||||
|
components: { MessageIncoming },
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<audio controls :src="src">{{ $t("fallbacks.audio_file") }}</audio>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageOutgoingAudioExport",
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-img :aspect-ratio="16 / 9" ref="image" :src="src" :cover="cover" :contain="contain" />
|
||||||
|
</div>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageOutgoingImageExport",
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
src: null,
|
||||||
|
cover: true,
|
||||||
|
contain: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const info = this.event.getContent().info;
|
||||||
|
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
|
||||||
|
// be stickers and small emoji type things.
|
||||||
|
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
|
||||||
|
this.cover = true;
|
||||||
|
this.contain = false;
|
||||||
|
} else {
|
||||||
|
this.cover = false;
|
||||||
|
this.contain = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.src) {
|
||||||
|
const objectUrl = this.src;
|
||||||
|
this.src = null;
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||||
|
<div class="bubble image-bubble">
|
||||||
|
<v-responsive :aspect-ratio="16 / 9" class="ma-0 pa-0">
|
||||||
|
<video :src="src" controls class="w-100 h-100">
|
||||||
|
{{ $t("fallbacks.video_file") }}
|
||||||
|
</video>
|
||||||
|
</v-responsive>
|
||||||
|
</div>
|
||||||
|
</message-outgoing>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import attachmentMixin from "../attachmentMixin";
|
||||||
|
import MessageOutgoing from "../MessageOutgoing.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MessageOutgoingVideoExport",
|
||||||
|
extends: MessageOutgoing,
|
||||||
|
components: { MessageOutgoing },
|
||||||
|
mixins: [attachmentMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/assets/css/chat.scss";
|
||||||
|
</style>
|
||||||
|
|
@ -62,7 +62,16 @@ export default {
|
||||||
return this.room.shouldEncryptForInvitedMembers()
|
return this.room.shouldEncryptForInvitedMembers()
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
userCanExportChat() {
|
||||||
|
// We say that if you can redact events, you are allowed to export chats.
|
||||||
|
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
|
||||||
|
let isAdmin =
|
||||||
|
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
|
||||||
|
return isAdmin;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
room: {
|
room: {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ var _browserCanRecordAudioF = function () {
|
||||||
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
||||||
|
|
||||||
class Util {
|
class Util {
|
||||||
getAttachment(matrixClient, event, progressCallback) {
|
getAttachment(matrixClient, event, progressCallback, asBlob = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (content.url != null) {
|
if (content.url != null) {
|
||||||
|
|
@ -53,7 +53,11 @@ class Util {
|
||||||
return this.decryptIfNeeded(file, response);
|
return this.decryptIfNeeded(file, response);
|
||||||
})
|
})
|
||||||
.then(bytes => {
|
.then(bytes => {
|
||||||
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
|
if (asBlob) {
|
||||||
|
resolve(new Blob([bytes.buffer], { type: file.mimetype }));
|
||||||
|
} else {
|
||||||
|
resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log("Download error: ", err);
|
console.log("Download error: ", err);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue