Add "scroll to end" button

Work on issue #15.
This commit is contained in:
N-Pex 2021-02-17 11:59:07 +01:00
parent b8b67a1d88
commit c44c5c714d
2 changed files with 210 additions and 117 deletions

View file

@ -109,6 +109,7 @@ $admin-fg: white;
}
.input-area-outer {
position: relative;
background-color: #ffffff;
margin: 0;
padding-left: 2 * $chat-standard-padding-s;
@ -149,6 +150,12 @@ $admin-fg: white;
}
}
.scroll-to-end {
position:absolute;
top:-64px;
right:16px;
}
.op-button {
position: relative;
display: inline-block;

View file

@ -9,19 +9,19 @@
@click.prevent="closeContextMenuIfOpen"
>
<div ref="messageOperationsStrut" class="message-operations-strut">
<message-operations
:style="opStyle"
v-on:close="showContextMenu = false"
v-if="selectedEvent && showContextMenu"
v-on:addreaction="addReaction"
v-on:addreply="addReply(selectedEvent)"
v-on:edit="edit(selectedEvent)"
v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)"
:event="selectedEvent"
:incoming="selectedEvent.getSender() != $matrix.currentUserId"
/>
</div>
<message-operations
:style="opStyle"
v-on:close="showContextMenu = false"
v-if="selectedEvent && showContextMenu"
v-on:addreaction="addReaction"
v-on:addreply="addReply(selectedEvent)"
v-on:edit="edit(selectedEvent)"
v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)"
:event="selectedEvent"
:incoming="selectedEvent.getSender() != $matrix.currentUserId"
/>
</div>
<!-- Handle resizes, e.g. when soft keyboard is shown/hidden -->
<resize-observer
@ -29,11 +29,18 @@
@notify="handleChatContainerResize"
/>
<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="dayForEvent(event)" />
<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="dayForEvent(event)"
/>
<div
v-if="
!event.isRelation() && !event.isRedacted() && !event.isRedaction()
@ -68,7 +75,11 @@
v-on:context-menu="showContextMenuForEvent($event)"
/>
<!-- <div>EventID: {{ event.getId() }}</div> -->
<div v-if="event.getId() == readMarker && index < (events.length - 1)" class="read-marker" title="Unread messages" />
<div
v-if="event.getId() == readMarker && index < events.length - 1"
class="read-marker"
title="Unread messages"
/>
</div>
</div>
</div>
@ -76,8 +87,23 @@
<!-- Input area -->
<v-container v-if="room" fluid class="input-area-outer">
<!-- "Scroll to end"-button -->
<v-btn
class="scroll-to-end"
v-show="showScrollToEnd"
fab
small
elevation="0"
color="black"
@click.stop="smoothScrollToEnd"
>
<v-icon color="white">arrow_downward</v-icon>
</v-btn>
<v-row class="ma-0 pa-0">
<div v-if="replyToEvent">REPLYING TO EVENT: {{ replyToEvent.getContent().body }}</div>
<div v-if="replyToEvent">
REPLYING TO EVENT: {{ replyToEvent.getContent().body }}
</div>
<!-- CONTACT IS TYPING -->
<div class="typing">
@ -87,7 +113,13 @@
<v-row class="input-area-inner align-center">
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1">
<label icon flat ref="attachmentLabel">
<v-btn icon large color="black" @click="showAttachmentPicker" :disabled="attachButtonDisabled">
<v-btn
icon
large
color="black"
@click="showAttachmentPicker"
:disabled="attachButtonDisabled"
>
<v-icon x-large>add_circle_outline</v-icon>
</v-btn>
<input
@ -114,18 +146,40 @@
placeholder="Send message"
hide-details
background-color="white"
v-on:keydown.enter.prevent="() => { sendMessage() }"
v-on:keydown.enter.prevent="
() => {
sendMessage();
}
"
/>
</v-col>
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-if="editedEvent || replyToEvent">
<v-btn fab small elevation="0" color="black" @click.stop="cancelEditReply">
<v-col
class="input-area-button text-center flex-grow-0 flex-shrink-1"
v-if="editedEvent || replyToEvent"
>
<v-btn
fab
small
elevation="0"
color="black"
@click.stop="cancelEditReply"
>
<v-icon color="white">cancel</v-icon>
</v-btn>
</v-col>
<v-col class="input-area-button text-center flex-grow-0 flex-shrink-1">
<v-btn fab small elevation="0" color="black" @click.stop="sendMessage" :disabled="sendButtonDisabled">
<v-icon color="white">{{ editedEvent ? 'save' : 'arrow_upward' }}</v-icon>
<v-btn
fab
small
elevation="0"
color="black"
@click.stop="sendMessage"
:disabled="sendButtonDisabled"
>
<v-icon color="white">{{
editedEvent ? "save" : "arrow_upward"
}}</v-icon>
</v-btn>
</v-col>
</v-row>
@ -169,19 +223,21 @@
</div>
<!-- "NOT ALLOWED FOR GUEST ACCOUNTS" dialog -->
<v-dialog v-model="showNotAllowedForGuests" class="ma-0 pa-0" width="50%">
<v-card>
<v-card-title>You are logged in as a guest</v-card-title>
<v-card-text>
<div>Unfortunately guests are not allowed to upload files.</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="showNotAllowedForGuests = false">Ok</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showNotAllowedForGuests" class="ma-0 pa-0" width="50%">
<v-card>
<v-card-title>You are logged in as a guest</v-card-title>
<v-card-text>
<div>Unfortunately guests are not allowed to upload files.</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="showNotAllowedForGuests = false"
>Ok</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
@ -281,6 +337,12 @@ export default {
*/
chatContainerSize: 0,
/**
* True if we should show the "scroll to end" marker in the chat. For now at least, we use a simple
* method here, basically just "if we can scroll, show it".
*/
showScrollToEnd: false,
/** Shows a dialog with info about an operation being disallowed for guests */
showNotAllowedForGuests: false,
@ -333,17 +395,24 @@ export default {
// If we have sent a RR, use that as read marker (so we don't have to wait for server round trip)
return this.lastRR.getId();
}
return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false);
return (
this.fullyReadMarker ||
this.room.getEventReadUpTo(this.$matrix.currentUserId, false)
);
},
fullyReadMarker() {
const readEvent = this.room.getAccountData('m.fully_read');
const readEvent = this.room.getAccountData("m.fully_read");
if (readEvent) {
return readEvent.getContent().event_id;
}
return null;
},
attachButtonDisabled() {
return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0;
return (
this.editedEvent != null ||
this.replyToEvent != null ||
this.currentInput.length > 0
);
},
sendButtonDisabled() {
return this.currentInput.length == 0;
@ -373,8 +442,7 @@ export default {
}
}
return "top:" + top + "px;left:" + left + "px";
}
},
},
watch: {
@ -384,7 +452,9 @@ export default {
if (value && value == oldValue) {
return; // No change.
}
console.log("Chat: Current room changed to " + (value ? value : "null"));
console.log(
"Chat: Current room changed to " + (value ? value : "null")
);
// Clear old events
this.events = [];
@ -398,76 +468,84 @@ export default {
if (!this.room) {
// Public room?
if (this.roomId && this.roomId.startsWith('#')) {
if (this.roomId && this.roomId.startsWith("#")) {
this.onRoomNotJoined();
}
return; // no room
}
// Joined?
if (this.room.hasMembershipState(this.currentUser.user_id, "join")) {
// Yes, load everything
this.onRoomJoined();
} else {
this.onRoomNotJoined();
}
}
// Joined?
if (this.room.hasMembershipState(this.currentUser.user_id, "join")) {
// Yes, load everything
this.onRoomJoined();
} else {
this.onRoomNotJoined();
}
},
},
},
methods: {
onRoomJoined() {
var initialEventId = this.readMarker;
console.log("Read up to " + initialEventId);
//initialEventId = null;
this.timelineWindow = new TimelineWindow(
this.$matrix.matrixClient,
this.room.getUnfilteredTimelineSet(),
{}
this.$matrix.matrixClient,
this.room.getUnfilteredTimelineSet(),
{}
);
const self = this;
this.timelineWindow.load(initialEventId, 20).then(() => {
console.log("This is", self);
self.events = self.timelineWindow.getEvents();
self.events = self.timelineWindow.getEvents();
const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer;
if (container.scrollHeight <= container.clientHeight &&
self.timelineWindow &&
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)) {
return self.timelineWindow.paginate(EventTimeline.BACKWARDS, 10, true)
.then(success => {
if (success) {
self.events = self.timelineWindow.getEvents();
return _getMoreIfNeeded.call(self);
} else {
return Promise.reject("Failed to paginate");
}
});
} else {
return Promise.resolve("Done");
}
}.bind(self);
const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer;
if (
container.scrollHeight <= container.clientHeight &&
self.timelineWindow &&
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
) {
return self.timelineWindow
.paginate(EventTimeline.BACKWARDS, 10, true)
.then((success) => {
if (success) {
self.events = self.timelineWindow.getEvents();
return _getMoreIfNeeded.call(self);
} else {
return Promise.reject("Failed to paginate");
}
});
} else {
return Promise.resolve("Done");
}
}.bind(self);
getMoreIfNeeded()
.catch(err => {
getMoreIfNeeded()
.catch((err) => {
console.log("ERROR " + err);
})
.finally(() => {
self.initialLoadDone = true;
if (initialEventId) {
self.scrollToEvent(initialEventId);
}
self.restartRRTimer();
if (initialEventId) {
self.scrollToEvent(initialEventId);
}
self.restartRRTimer();
});
});
});
},
onRoomNotJoined() {
this.$navigation.push({ name: "Join", params: { roomId: util.sanitizeRoomId(this.roomAliasOrId) }}, 0);
this.$navigation.push(
{
name: "Join",
params: { roomId: util.sanitizeRoomId(this.roomAliasOrId) },
},
0
);
},
touchX(event) {
@ -594,6 +672,10 @@ export default {
) {
this.handleScrolledToBottom(false);
}
this.showScrollToEnd =
container.scrollHeight - container.scrollTop.toFixed(0) >
container.clientHeight;
this.restartRRTimer();
},
onEvent(event) {
@ -631,7 +713,7 @@ export default {
} else {
const index = this.typingMembers.indexOf(member);
if (index > -1) {
this.typingMembers.splice(index, 1);
this.typingMembers.splice(index, 1);
}
}
console.log("Typing: ", this.typingMembers);
@ -669,7 +751,7 @@ export default {
// this.showNotAllowedForGuests = true;
// return;
// }
this.$refs.attachment.click()
this.$refs.attachment.click();
},
/**
@ -776,8 +858,8 @@ export default {
},
/**
* Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom.
*/
* 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 ref = this.$refs[eventId];
@ -825,13 +907,14 @@ export default {
},
redact(event) {
this.$matrix.matrixClient.redactEvent(event.getRoomId(), event.getId())
.then(() => {
console.log("Message redacted");
})
.catch(err => {
console.log("Redaction failed: ", err);
})
this.$matrix.matrixClient
.redactEvent(event.getRoomId(), event.getId())
.then(() => {
console.log("Message redacted");
})
.catch((err) => {
console.log("Redaction failed: ", err);
});
},
download(event) {
@ -906,10 +989,10 @@ export default {
this.rrTimer = null;
}
},
/**
* Start/restart the timer to Read Receipts.
*/
* Start/restart the timer to Read Receipts.
*/
restartRRTimer() {
this.stopRRTimer();
this.rrTimer = setInterval(this.rrTimerElapsed, READ_RECEIPT_TIMEOUT);
@ -919,30 +1002,33 @@ export default {
const container = this.$refs.chatContainer;
const el = util.getLastVisibleElement(container);
if (el) {
const eventId = el.getAttribute('eventId');
const eventId = el.getAttribute("eventId");
if (eventId && this.room) {
const event = this.room.findEventById(eventId);
if (event && (!this.lastRR || event.getTs() > this.lastRR.getTs())) {
// Disable timer while we are sending
clearInterval(this.rrTimer);
this.rrTimer = null;
// Send read receipt
this.$matrix.matrixClient.sendReadReceipt(event)
.then(() => {
this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, eventId)
})
.then(() => {
console.log("RR sent for event: " + eventId);
this.lastRR = event;
})
.catch(err => {
console.log("Failed to update read marker: ", err);
})
.finally(() => {
this.restartRRTimer();
});
this.$matrix.matrixClient
.sendReadReceipt(event)
.then(() => {
this.$matrix.matrixClient.setRoomReadMarkers(
this.room.roomId,
eventId
);
})
.then(() => {
console.log("RR sent for event: " + eventId);
this.lastRR = event;
})
.catch((err) => {
console.log("Failed to update read marker: ", err);
})
.finally(() => {
this.restartRRTimer();
});
}
}
}
@ -959,7 +1045,7 @@ export default {
dayForEvent(event) {
return util.formatDay(event.getTs());
}
},
},
};
</script>