@@ -274,6 +292,8 @@
+
{{ currentSendError }}
@@ -281,7 +301,7 @@
{{ $t("menu.cancel") }}
- {{ $t("menu.send") }}
@@ -360,7 +380,7 @@ import UserProfileDialog from "./UserProfileDialog.vue"
import BottomSheet from "./BottomSheet.vue";
import ImageResize from "image-resize";
import CreatePollDialog from "./CreatePollDialog.vue";
-import chatMixin from "./chatMixin";
+import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
import sendAttachmentsMixin from "./sendAttachmentsMixin";
import AudioLayout from "./AudioLayout.vue";
import FileDropLayout from "./file_mode/FileDropLayout";
@@ -368,6 +388,7 @@ import roomTypeMixin from "./roomTypeMixin";
import roomMembersMixin from "./roomMembersMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageErrorHandler from "./MessageErrorHandler";
+import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue';
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
@@ -422,11 +443,15 @@ export default {
UserProfileDialog,
PurgeRoomDialog,
WelcomeHeaderChannelUser,
- MessageErrorHandler
+ MessageErrorHandler,
+ MessageOperationsChannel
},
data() {
return {
+ ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
+ ROOM_READ_MARKER_EVENT_PLACEHOLDER: ROOM_READ_MARKER_EVENT_PLACEHOLDER,
+
waitingForRoomObject: false,
events: [],
currentInput: "",
@@ -442,6 +467,7 @@ export default {
currentSendShowSendButton: true,
currentSendError: null,
currentSendErrorExceededFile: null,
+ attachmentCaption: undefined,
showEmojiPicker: false,
selectedEvent: null,
editedEvent: null,
@@ -512,7 +538,8 @@ export default {
heartPosition: {
top: 0,
left: 0
- }
+ },
+ reverseOrder: false
};
},
@@ -569,6 +596,12 @@ export default {
isCurrentFileInputsAnArray() {
return Array.isArray(this.currentFileInputs)
},
+ showAttachmentCaptionInput() {
+ // IFF we are sending one PDF, add option to set caption.
+ const imageFiles = this.imageFiles || [];
+ const otherFiles = this.nonImageFiles || [];
+ return imageFiles.length == 0 && otherFiles.length == 1 && (otherFiles[0].type === "application/pdf" || (otherFiles[0].name || "").endsWith(".pdf"));
+ },
currentFileInputsDialog: {
get() {
return this.isCurrentFileInputsAnArray
@@ -680,6 +713,8 @@ export default {
},
filteredEvents() {
+ let events = this.events;
+
if (this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId)) {
if (this.room.getHistoryVisibility() == "joined") {
// For encrypted rooms where history is set to "joined" we can't read old events.
@@ -692,12 +727,46 @@ export default {
(!e.getPrevContent() || e.getPrevContent().membership != "join") &&
e.getStateKey() == this.$matrix.currentUserId) {
// Our own join event.
- return this.events.slice(idx + 1);
+ events = this.events.slice(idx + 1);
}
}
}
}
- return this.events;
+
+ // Filter out relations and redactions
+ events = this.events.toReversed().filter((e) => !e.isRelation() && !e.isRedaction());
+
+ // If Channel, remove all redacted events as well.
+ if (this.room && this.room.displayType == ROOM_TYPE_CHANNEL) {
+ events = events.filter((e) => !e.isRedacted());
+ }
+
+ // Add read marker, if it is not newer than the "latest" message we are going to display
+ //
+ let showReadMarker = false;
+ let lastDisplayedEvent = undefined;
+ events = events.flatMap((e) => {
+ let result = [];
+ Vue.set(e, "component", this.componentForEvent(e, false));
+ if (e.getId() == this.readMarker && showReadMarker) {
+ const readMarkerEvent = ROOM_READ_MARKER_EVENT_PLACEHOLDER;
+ Vue.set(readMarkerEvent, "component", this.componentForEvent(readMarkerEvent, false));
+ if (readMarkerEvent.component) {
+ Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
+ }
+ result.push(readMarkerEvent);
+ }
+ if (e.component) {
+ Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
+ lastDisplayedEvent = e;
+ if (e.getSender() !== this.$matrix.currentUserId) {
+ showReadMarker = true;
+ }
+ }
+ result.push(e);
+ return result;
+ })
+ return (this.reverseOrder ? events : events.toReversed()) // Reverse back if needed
},
roomCreatedByUsRecently() {
@@ -732,6 +801,12 @@ export default {
}
return null;
},
+ messageOperationsComponent() {
+ if (this.room.displayType == ROOM_TYPE_CHANNEL) {
+ return MessageOperationsChannel;
+ }
+ return MessageOperations;
+ },
chatContainerStyle() {
if (this.$config.chat_backgrounds && this.room && this.roomId) {
const roomType = this.isDirectRoom ? "direct" : this.isPublicRoom ? "public" : "invite";
@@ -843,7 +918,7 @@ export default {
this.onRoomNotJoined();
} else {
if (this.room) {
- this.onRoomJoined(this.readMarker);
+ this.onRoomJoined(this.roomDisplayType == ROOM_TYPE_CHANNEL ? null : this.readMarker);
} else {
this.waitingForRoomObject = true;
return; // no room, wait for it (we know we are joined so need to wait for sync to complete)
@@ -860,7 +935,7 @@ export default {
// Were we waiting?
if (this.room && this.room.roomId == this.roomId && this.waitingForRoomObject) {
this.waitingForRoomObject = false;
- this.onRoomJoined(this.readMarker);
+ this.onRoomJoined(this.roomDisplayType == ROOM_TYPE_CHANNEL ? null : this.readMarker);
}
},
showMessageOperations(show) {
@@ -876,6 +951,12 @@ export default {
var rectAnchor = this.showContextMenuAnchor.getBoundingClientRect();
var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
+ if (this.room.displayType == ROOM_TYPE_CHANNEL) {
+ top = rectAnchor.top - rectChat.top;
+ let right = rectChat.right - rectAnchor.right;
+ this.opStyle = "top:" + top + "px;right:" + right + "px";
+ return;
+ } else {
top = rectAnchor.top - rectChat.top - 50;
left = rectAnchor.left - rectChat.left - 75;
if (left + rectOps.width + 10 >= rectChat.right) {
@@ -884,6 +965,7 @@ export default {
left = 0;
}
}
+ }
}
this.opStyle = "top:" + top + "px;left:" + left + "px";
});
@@ -906,10 +988,33 @@ export default {
},
/**
- * Set events to display. At the same time, filter out messages that are past rentention period etc.
+ * Set events to display. At the same time, filter out messages that are past rentention period etc. Also, filter pinned events "at the top"
*/
- setEvents(events) {
- this.events = this.filterOutOldAndInvisible(events);
+ setEvents(events, onlyIfLengthChanges = false) {
+ let updated = this.filterOutOldAndInvisible(events);
+
+ // Handle pinning
+ //
+ if (this.room) {
+ const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
+ updated.forEach((e) => {
+ Vue.set(e, "isPinned", pinnedEvents.includes(e.threadParent ? e.threadParent.getId() : e.getId()));
+ Vue.set(e, "isChannelMessage", (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL));
+ });
+
+ updated = updated.sort((e1, e2) => {
+ if (!e1.isPinned && !e2.isPinned) return 0;
+ else if (e1.isPinned && !e2.isPinned) return this.reverseOrder ? 1 : -1;
+ else if (e2.isPinned && !e1.isPinned) return this.reverseOrder ? -1 : 1;
+ else {
+ // Look at order in "pinned" value in the m.room.pinned_events event!
+ return pinnedEvents.indexOf(e1.getId()) < pinnedEvents.indexOf(e2.getId()) ? (this.reverseOrder ? 1 : -1) : (this.reverseOrder ? -1 : 1)
+ }
+ });
+ }
+ if (!onlyIfLengthChanges || updated.length != this.events.length) {
+ this.events = updated; // Changed
+ }
},
filterOutOldAndInvisible(events) {
@@ -952,10 +1057,7 @@ export default {
},
onRetentionTimer() {
- const events = this.filterOutOldAndInvisible(this.events);
- if (events.length != this.events.length) {
- this.events = events; // Changed
- }
+ this.setEvents(this.events, true);
},
onRoomJoined(initialEventId) {
@@ -969,6 +1071,9 @@ export default {
this.newlyJoinedRoom = joinEvent.getLocalAge() < 1 * 60000 /* 1 minute */;
}
+ this.reverseOrder = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL);
+ Vue.set(this.room, "displayType", this.roomDisplayType);
+
// Listen to events
this.$matrix.on("Room.timeline", this.onEvent);
this.$matrix.on("RoomMember.typing", this.onUserTyping);
@@ -1080,7 +1185,7 @@ export default {
});
} else {
// Can't paginate, just scroll to bottom of window!
- this.smoothScrollToEnd();
+ this.smoothScrollToLatest();
}
},
@@ -1150,7 +1255,7 @@ export default {
this.$nextTick(() => {
const container = this.chatContainer;
if (container && container.scrollHeight <= container.clientHeight) {
- this.handleScrolledToTop();
+ this.handleScrolledToOldest();
}
});
},
@@ -1162,15 +1267,23 @@ export default {
const bufferHeight = container.clientHeight * WINDOW_BUFFER_SIZE;
if (container.scrollTop <= bufferHeight) {
// Scrolled to top
- this.handleScrolledToTop();
+ if (this.reverseOrder) {
+ this.handleScrolledToLatest(false);
+ } else {
+ this.handleScrolledToOldest();
+ }
} else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
- this.handleScrolledToBottom(false);
+ if (this.reverseOrder) {
+ this.handleScrolledToOldest();
+ } else {
+ this.handleScrolledToLatest(false);
+ }
}
this.showScrollToEnd =
container.scrollHeight === container.clientHeight
? false
- : container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight ||
+ : (this.reverseOrder ? (container.scrollTop.toFixed(0) > 0) : (container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight)) ||
(this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
this.restartRRTimer();
@@ -1262,14 +1375,17 @@ export default {
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread)) {
// If we are at bottom, scroll to see new events...
- var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
+ var scrollToSeeNew = !event.isRedaction() && event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
const container = this.chatContainer;
if (container) {
- if (container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
+ if (this.reverseOrder && container.scrollTop.toFixed(0) == 0) {
+ scrollToSeeNew = true;
+ }
+ else if (!this.reverseOrder && container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
scrollToSeeNew = true;
}
}
- this.handleScrolledToBottom(scrollToSeeNew);
+ this.handleScrolledToLatest(scrollToSeeNew);
// If kick or ban event, redirect to "goodbye"...
if (event.getType() === "m.room.member" &&
@@ -1441,6 +1557,7 @@ export default {
const promise = this.sendAttachments(text, this.currentFileInputs);
promise.then(() => {
this.currentFileInputs = null;
+ this.attachmentCaption = undefined;
this.sendingStatus = this.sendStatuses.INITIAL;
})
.catch((err) => {
@@ -1457,8 +1574,11 @@ export default {
cancelSendAttachment() {
this.$refs.attachment.value = null;
- this.cancelSendAttachments();
+ if (this.sendingStatus != this.sendStatuses.INITIAL) {
+ this.cancelSendAttachments();
+ }
this.currentFileInputs = null;
+ this.attachmentCaption = undefined;
this.currentSendError = null;
this.currentSendErrorExceededFile = null;
this.sendingStatus = this.sendStatuses.INITIAL;
@@ -1481,7 +1601,7 @@ export default {
* @param {*} element Root element for the chat message.
*/
onLayoutChange(action, element) {
- if (!element || !element.parentElemen || this.useVoiceMode || this.useFileModeNonAdmin) {
+ if (!element || !element.parentElement || this.useVoiceMode || this.useFileModeNonAdmin) {
action();
return
}
@@ -1494,7 +1614,7 @@ export default {
});
},
- handleScrolledToTop() {
+ handleScrolledToOldest() {
if (
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) &&
@@ -1505,7 +1625,7 @@ export default {
.paginate(EventTimeline.BACKWARDS, 10, true)
.then((success) => {
if (success && this.scrollPosition) {
- this.scrollPosition.prepareFor("up");
+ this.scrollPosition.prepareFor(this.reverseOrder ? "down" : "up");
this.setEvents(this.timelineWindow.getEvents());
this.$nextTick(() => {
// restore scroll position!
@@ -1520,7 +1640,7 @@ export default {
}
},
- handleScrolledToBottom(scrollToEnd) {
+ handleScrolledToLatest(smoothScrollToLatest) {
if (
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS) &&
@@ -1533,13 +1653,13 @@ export default {
if (success) {
this.setEvents(this.timelineWindow.getEvents());
if (!this.useVoiceMode && this.scrollPosition) {
- this.scrollPosition.prepareFor("down");
+ this.scrollPosition.prepareFor(this.reverseOrder ? "up" : "down");
this.$nextTick(() => {
// restore scroll position!
console.log("Restore scroll!");
this.scrollPosition.restore();
- if (scrollToEnd) {
- this.smoothScrollToEnd();
+ if (smoothScrollToLatest) {
+ this.smoothScrollToLatest();
}
});
}
@@ -1555,6 +1675,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) {
+ console.log("Scroll to event", eventId);
const container = this.chatContainer;
const ref = this.$refs[eventId];
if (container && ref) {
@@ -1562,7 +1683,7 @@ export default {
const item = ref[0].getBoundingClientRect();
let offsetY = (parent.bottom - parent.top) / 2;
if (ref[0].clientHeight > offsetY) {
- offsetY = Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
+ offsetY = this.reverseOrder ? 0 : Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
}
const targetY = parent.top + offsetY;
const currentY = item.top;
@@ -1573,10 +1694,21 @@ export default {
}
},
- smoothScrollToEnd() {
+ smoothScrollToLatest() {
this.$nextTick(function () {
const container = this.chatContainer;
if (container && container.children.length > 0) {
+ if (this.reverseOrder) {
+ const firstChild = container.children[0];
+ console.log("Scroll into view", firstChild);
+ window.requestAnimationFrame(() => {
+ firstChild.scrollIntoView({
+ behavior: "smooth",
+ block: "end",
+ inline: "nearest",
+ });
+ });
+ } else {
const lastChild = container.children[container.children.length - 1];
console.log("Scroll into view", lastChild);
window.requestAnimationFrame(() => {
@@ -1587,6 +1719,7 @@ export default {
});
});
}
+ }
});
},
@@ -1668,6 +1801,16 @@ export default {
}
},
+ pin(event) {
+ const eventToPin = event.parentThread ? event.parentThread : event;
+ this.$matrix.setEventPinned(this.room, eventToPin, true);
+ },
+
+ unpin(event) {
+ const eventToUnpin = event.parentThread ? event.parentThread : event;
+ this.$matrix.setEventPinned(this.room, eventToUnpin, false);
+ },
+
cancelEditReply() {
this.currentInput = "";
this.editedEvent = null;
@@ -1752,14 +1895,15 @@ export default {
showContextMenuForEvent(e) {
const event = e.event;
if (this.selectedEvent == event) {
+ this.showContextMenuAnchor = e.anchor;
this.showContextMenu = !this.showContextMenu;
} else {
this.showContextMenu = false;
this.$nextTick(() => {
this.selectedEvent = event;
this.updateRecentEmojis();
- this.showContextMenu = true;
this.showContextMenuAnchor = e.anchor;
+ this.showContextMenu = true;
})
}
},
@@ -1801,74 +1945,62 @@ export default {
if (this.$matrix.currentRoomBeingPurged) {
return;
}
-
- let eventIdFirst = null;
- let eventIdLast = null;
if (!this.useVoiceMode && !this.useFileModeNonAdmin) {
- 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);
+ this.rrTimer = setTimeout(() => { this.rrTimerElapsed() }, READ_RECEIPT_TIMEOUT);
}
},
- rrTimerElapsed(eventIdFirst, eventIdLast) {
+ rrTimerElapsed() {
this.rrTimer = null;
- this.sendRR(eventIdFirst, eventIdLast);
+ this.sendRR();
this.restartRRTimer();
},
- 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);
+ sendRR(onlyTheseEvents) {
+ let 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();
- }
+ let eventsToConsider = onlyTheseEvents;
+ if (!eventsToConsider) {
+ // If events to consider is not given (used by Audio layout) then consider all visible events (with some restrictions).
+ let visibleEventIds = util.findVisibleElements(this.chatContainer).filter(el => el.hasAttribute("eventId")).map(el => el.getAttribute("eventId"));
- 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
+ eventsToConsider = this.events.filter(e => {
+ if (e.getTs() > lastTimestamp &&
+ // Make sure it's not a local echo event...
+ !e.getId().startsWith("~")) {
+ if (e.isRelation()) {
+ return visibleEventIds.includes(e.getAssociatedId());
+ } else if (e.getType() == "m.room.pinned_events") {
+ // Ignore the pin event. If we pin an older message it does not automatically mean we have read the
+ // messages between.
+ return false;
+ } else {
+ return visibleEventIds.includes(e.getId());
+ }
}
+ return false;
+ });
+ }
- // Stop iterating at first visible
- if (event.getId() == eventIdFirst) {
- break;
- }
- }
+ eventsToConsider.sort((a, b) => b.getTs() - a.getTs());
+ if (eventsToConsider.length > 0 && eventsToConsider[0] != this.lastRR) {
+ const eventToSendRRFor = eventsToConsider[0];
+
+ // Send read receipt
+ this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, eventToSendRRFor.getId(), eventToSendRRFor)
+ .then(() => {
+ console.log("RR sent for event", eventToSendRRFor.getId(), eventToSendRRFor.getType());
+ this.lastRR = eventToSendRRFor;
+ })
+ .catch((err) => {
+ console.log("Failed to update read marker: ", err);
+ })
+ .finally(() => {
+ this.restartRRTimer();
+ });
}
},
@@ -1963,13 +2095,21 @@ export default {
audioPlaybackEnded(matrixEventId) {
if (!this.useVoiceMode) { // Voice mode has own autoplay handling inside "AudioLayout"!
// Auto play consecutive audio messages, either incoming or sent.
- const filteredEvents = this.filteredEvents;
- const index = filteredEvents.findIndex(e => e.getId() === matrixEventId);
- if (index >= 0 && index < (filteredEvents.length - 1)) {
- const nextEvent = filteredEvents[index + 1];
- if (nextEvent.getContent().msgtype === "m.audio") {
- // Yes, audio event!
- this.$audioPlayer.play(nextEvent, this.timelineSet);
+ let event = this.events.find(e => e.getId() === matrixEventId);
+ if (event) {
+ event = event.parentThread ? event.parentThread : event;
+ if (event.nextDisplayedEvent) {
+ const nextEvent = event.nextDisplayedEvent;
+ if (nextEvent.getContent().msgtype === "m.audio") {
+ // Yes, audio event!
+ this.$audioPlayer.play(nextEvent, this.timelineSet);
+ } else if (nextEvent.isMxThread) {
+ const children = this.timelineSet.relations.getAllChildEventsForEvent(nextEvent.getId());
+ if (children && children.length > 0 && children[0].getContent().msgtype === "m.audio") {
+ // Yes, audio event!
+ this.$audioPlayer.play(children[0], this.timelineSet);
+ }
+ }
}
}
}
diff --git a/src/components/InputControl.vue b/src/components/InputControl.vue
index fb74749..539831c 100644
--- a/src/components/InputControl.vue
+++ b/src/components/InputControl.vue
@@ -1,21 +1,18 @@
@@ -29,25 +26,25 @@ export default {
props: {
label: {
type: String,
- default: function() {
+ default: function () {
return "";
},
},
error: {
type: String,
- default: function() {
+ default: function () {
return "";
},
},
disabled: {
type: Boolean,
- default: function() {
+ default: function () {
return false;
},
},
modelValue: {
type: String,
- default: function() {
+ default: function () {
return null;
},
},
@@ -73,12 +70,20 @@ export default {
width: 100%;
display: flex;
position: relative;
- min-height: $min-touch-target;
+ min-height: 58px;
background: #f5f5f5;
border-radius: 4px;
transition: background-color 0.4s, box-shadow 0.4s;
margin-bottom: 12px;
+
+ .input-wrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ padding: 22px 16px 0px 16px;
+ }
+
&.errored {
border: 1px solid #e31b00;
}
@@ -95,6 +100,7 @@ export default {
right: 16px;
display: flex;
}
+
.placeholder {
text-align: start;
font-family: "Inter";
@@ -122,10 +128,11 @@ export default {
position: absolute;
left: 0px;
right: 0px;
- top: 14px;
+ top: 20px;
font-size: 16px;
color: rgba(0, 0, 0, 0.7);
margin-right: 0px;
+
&:only-child {
top: 8px;
}
@@ -139,16 +146,12 @@ export default {
line-height: 117%;
letter-spacing: 0.4px;
color: #000000;
- position: absolute;
- top: 20px;
- left: 16px;
- right: 16px;
- bottom: 0;
- width: 100%;
+ flex: 1 1 100%;
&:focus {
top: 20px;
}
+
&:focus,
&:focus-visible {
border: none;
@@ -157,14 +160,7 @@ export default {
}
.slot {
- position: absolute;
- right: 16px;
- align-self: center;
-
- [dir="rtl"] & {
- left: 16px;
- right: unset;
- }
+ flex: 0 0 auto;
}
}
diff --git a/src/components/MoreLessControl.vue b/src/components/MoreLessControl.vue
new file mode 100644
index 0000000..52693a7
--- /dev/null
+++ b/src/components/MoreLessControl.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
{{
+ $t('global.show_more') }}
+
{{
+ $t('global.show_less') }}
+
+
+
+
+
+
diff --git a/src/components/RoomExport.vue b/src/components/RoomExport.vue
index 6038c77..3750274 100644
--- a/src/components/RoomExport.vue
+++ b/src/components/RoomExport.vue
@@ -187,6 +187,7 @@ export default {
this.cancelled = true;
},
async getEvents() {
+ // TODO - Handle pinned messages?
const eventsPerBatch = 100;
let batchToken = null;
var nToFetch = null;
diff --git a/src/components/RoomInfo.vue b/src/components/RoomInfo.vue
index a43d74b..59b4311 100644
--- a/src/components/RoomInfo.vue
+++ b/src/components/RoomInfo.vue
@@ -202,7 +202,7 @@
}}
- {{ `$vuetify.icons.${isAdmin(member)? 'make_admin' : 'make_moderator'}` }}
+ {{ `$vuetify.icons.${isAdmin(member)? 'make_admin' : 'make_moderator'}` }}