Timeline window and initial back paging
This commit is contained in:
parent
854a5ec770
commit
6c563b1e51
4 changed files with 178 additions and 83 deletions
|
|
@ -128,7 +128,7 @@ $chat-text-size: 0.7pt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender {
|
.sender, .status {
|
||||||
font-family: 'Titillium Web', sans-serif;
|
font-family: 'Titillium Web', sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 15 * $chat-text-size;
|
font-size: 15 * $chat-text-size;
|
||||||
|
|
@ -151,3 +151,12 @@ $chat-text-size: 0.7pt;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #1c242a;
|
color: #1c242a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusEvent {
|
||||||
|
font-family: 'Titillium Web', sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 15 * $chat-text-size;
|
||||||
|
color: #1c242a;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
class="chat-content flex-grow-1 flex-shrink-1"
|
class="chat-content flex-grow-1 flex-shrink-1"
|
||||||
ref="chatContainer"
|
ref="chatContainer"
|
||||||
style="overflow-x: hidden; overflow-y: auto"
|
style="overflow-x: hidden; overflow-y: auto"
|
||||||
|
v-on:scroll="onScroll"
|
||||||
>
|
>
|
||||||
<div v-for="(event, index) in events" :key="index">
|
<div v-for="event in events" :key="event.eventId">
|
||||||
<!-- Contact joined the chat -->
|
<!-- Contact joined the chat -->
|
||||||
<div
|
<div
|
||||||
class="messageJoin"
|
class="messageJoin"
|
||||||
|
|
@ -44,12 +45,11 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
event.getSender() != myUserId &&
|
event.getSender() != myUserId && event.getType() == 'm.room.message'
|
||||||
event.getType() == 'm.room.message'
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="messageIn">
|
<div class="messageIn">
|
||||||
<div class="sender">{{ event.getSender() }}</div>
|
<div class="sender">{{ messageEventDisplayName(event) }}</div>
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="message">{{ event.getContent().body }}</div>
|
<div class="message">{{ event.getContent().body }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -64,11 +64,30 @@
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<div class="message">{{ event.getContent().body }}</div>
|
<div class="message">{{ event.getContent().body }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="status">{{ event.status }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="time">
|
<div class="time">
|
||||||
{{ formatTime(event.event.origin_server_ts) }}
|
{{ formatTime(event.event.origin_server_ts) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ROOM NAME CHANGED -->
|
||||||
|
<div v-else-if="event.getType() == 'm.room.name'" class="statusEvent">
|
||||||
|
{{ stateEventDisplayName(event) }} changed room name to {{ event.getContent().name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ROOM TOPIC CHANGED -->
|
||||||
|
<div v-else-if="event.getType() == 'm.room.topic'" class="statusEvent">
|
||||||
|
{{ stateEventDisplayName(event) }} changed topic to {{ event.getContent().topic }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ROOM AVATAR CHANGED -->
|
||||||
|
<div v-else-if="event.getType() == 'm.room.avatar'" class="statusEvent">
|
||||||
|
{{ stateEventDisplayName(event) }} changed the room avatar
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="statusEvent">Event: {{ event.getType() }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- CONTACT IS TYPING -->
|
<!-- CONTACT IS TYPING -->
|
||||||
<div v-show="contactIsTyping" class="typing">Someone is typing...</div>
|
<div v-show="contactIsTyping" class="typing">Someone is typing...</div>
|
||||||
|
|
@ -98,15 +117,59 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
|
||||||
|
function ScrollPosition(node) {
|
||||||
|
this.node = node;
|
||||||
|
this.previousScrollHeightMinusTop = 0;
|
||||||
|
this.readyFor = "up";
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollPosition.prototype.restore = function () {
|
||||||
|
if (this.readyFor === "up") {
|
||||||
|
this.node.scrollTop =
|
||||||
|
this.node.scrollHeight - this.previousScrollHeightMinusTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'down' doesn't need to be special cased unless the
|
||||||
|
// content was flowing upwards, which would only happen
|
||||||
|
// if the container is position: absolute, bottom: 0 for
|
||||||
|
// a Facebook messages effect
|
||||||
|
};
|
||||||
|
|
||||||
|
ScrollPosition.prototype.prepareFor = function (direction) {
|
||||||
|
this.readyFor = direction || "up";
|
||||||
|
this.previousScrollHeightMinusTop =
|
||||||
|
this.node.scrollHeight - this.node.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Chat",
|
name: "Chat",
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
room: null,
|
||||||
events: [],
|
events: [],
|
||||||
currentInput: "",
|
currentInput: "",
|
||||||
contactIsTyping: false,
|
contactIsTyping: false,
|
||||||
|
timelineWindow: null,
|
||||||
|
scrollPosition: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
const container = this.$refs.chatContainer;
|
||||||
|
this.scrollPosition = new ScrollPosition(container);
|
||||||
|
|
||||||
|
this.$matrix.on("Room.timeline", this.onEvent);
|
||||||
|
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
this.$matrix.off("Room.timeline", this.onEvent);
|
||||||
|
this.$matrix.off("RoomMember.typing", this.onUserTyping);
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
myUserId() {
|
myUserId() {
|
||||||
return this.$store.state.auth.user.user_id;
|
return this.$store.state.auth.user.user_id;
|
||||||
|
|
@ -125,95 +188,82 @@ export default {
|
||||||
|
|
||||||
// Clear old events
|
// Clear old events
|
||||||
this.events = [];
|
this.events = [];
|
||||||
|
this.timelineWindow = null;
|
||||||
// Remove all old room listeners
|
|
||||||
this.$matrix.off("Room.timeline", this.onEvent);
|
|
||||||
this.$matrix.off("RoomMember.typing", this.onUserTyping);
|
|
||||||
|
|
||||||
this.contactIsTyping = false;
|
this.contactIsTyping = false;
|
||||||
|
|
||||||
if (!this.roomId) {
|
if (!this.roomId) {
|
||||||
return; // no room
|
return; // no room
|
||||||
}
|
}
|
||||||
const room = this.$matrix.getRoom(this.roomId);
|
|
||||||
if (!room) {
|
this.room = this.$matrix.getRoom(this.roomId);
|
||||||
|
if (!this.room) {
|
||||||
return; // Not found
|
return; // Not found
|
||||||
}
|
}
|
||||||
|
|
||||||
room.timeline.forEach((event) => {
|
this.timelineWindow = new TimelineWindow(
|
||||||
this.handleMatrixEvent(event);
|
this.$matrix.matrixClient,
|
||||||
|
this.room.getUnfilteredTimelineSet(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
this.timelineWindow.load(null, 20).then(() => {
|
||||||
|
this.events = this.timelineWindow.getEvents();
|
||||||
|
this.paginateBackIfNeeded();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for this room
|
|
||||||
this.$matrix.on("Room.timeline", this.onEvent);
|
|
||||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
paginateBackIfNeeded() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const container = this.$refs.chatContainer;
|
||||||
|
if (container.scrollHeight <= container.clientHeight) {
|
||||||
|
this.handleScrolledToTop();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onScroll(ignoredevent) {
|
||||||
|
const container = this.$refs.chatContainer;
|
||||||
|
if (container.scrollTop == 0) {
|
||||||
|
// Scrolled to top
|
||||||
|
this.handleScrolledToTop();
|
||||||
|
} else if (
|
||||||
|
container.scrollHeight - container.scrollTop ==
|
||||||
|
container.clientHeight
|
||||||
|
) {
|
||||||
|
this.handleScrolledToBottom();
|
||||||
|
}
|
||||||
|
},
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
if (event.getRoomId() !== this.roomId) {
|
if (event.getRoomId() !== this.roomId) {
|
||||||
return; // Not for this room
|
return; // Not for this room
|
||||||
}
|
}
|
||||||
if (this.handleMatrixEvent(event)) {
|
this.paginateBackIfNeeded();
|
||||||
this.$nextTick(function () {
|
|
||||||
const container = this.$refs.chatContainer;
|
|
||||||
if (container.children.length > 0) {
|
|
||||||
const lastChild = container.children[container.children.length - 1];
|
|
||||||
console.log("Scroll into view", lastChild);
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
lastChild.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "end",
|
|
||||||
inline: "nearest",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onUserTyping(event) {
|
onUserTyping(event) {
|
||||||
//TODO
|
if (event.getRoomId() !== this.roomId) {
|
||||||
|
return; // Not for this room
|
||||||
|
}
|
||||||
console.log("Typing:", event);
|
console.log("Typing:", event);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a matrix event. Add it to our array if valid type.
|
|
||||||
* @returns True if the event was added, false otherwise.
|
|
||||||
*/
|
|
||||||
handleMatrixEvent(event) {
|
|
||||||
console.log("Type is", event.getType());
|
|
||||||
|
|
||||||
if (event.getType() == "m.room.encryption") {
|
|
||||||
this.$root.matrixClient.setRoomEncryption(
|
|
||||||
event.event.room_id,
|
|
||||||
event.getContent()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedEvents = [
|
|
||||||
"m.room.message",
|
|
||||||
"m.room.member",
|
|
||||||
"m.room.encrypted",
|
|
||||||
];
|
|
||||||
if (allowedEvents.includes(event.getType())) {
|
|
||||||
console.log("Add event", event);
|
|
||||||
this.events.push(event);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.log("Ignore event", event);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a display name given an event.
|
* Get a display name given an event.
|
||||||
*/
|
*/
|
||||||
stateEventDisplayName(event) {
|
stateEventDisplayName(event) {
|
||||||
|
if (this.room) {
|
||||||
|
const member = this.room.getMember(event.getSender());
|
||||||
|
if (member) {
|
||||||
|
return member.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
return event.getContent().displayname || event.event.state_key;
|
return event.getContent().displayname || event.event.state_key;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
messageEventDisplayName(event) {
|
||||||
|
return this.stateEventDisplayName(event);
|
||||||
|
},
|
||||||
|
|
||||||
sendMessage() {
|
sendMessage() {
|
||||||
if (this.currentInput.length > 0) {
|
if (this.currentInput.length > 0) {
|
||||||
this.sendMatrixMessage(this.currentInput);
|
this.sendMatrixMessage(this.currentInput);
|
||||||
|
|
@ -222,17 +272,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
sendMatrixMessage(body) {
|
sendMatrixMessage(body) {
|
||||||
// Send chat message sent event
|
|
||||||
window.logtag("event", "chat_message_sent", {
|
|
||||||
event_category: "chat",
|
|
||||||
});
|
|
||||||
|
|
||||||
var content = {
|
var content = {
|
||||||
body: body,
|
body: body,
|
||||||
msgtype: "m.notice",
|
msgtype: "m.text",
|
||||||
};
|
};
|
||||||
this.$root.matrixClient.sendEvent(
|
this.$matrix.matrixClient.sendEvent(
|
||||||
this.currentRoomId,
|
this.roomId,
|
||||||
"m.room.message",
|
"m.room.message",
|
||||||
content,
|
content,
|
||||||
"",
|
"",
|
||||||
|
|
@ -257,6 +302,33 @@ export default {
|
||||||
}
|
}
|
||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleScrolledToTop() {
|
||||||
|
console.log("@top");
|
||||||
|
// const room = this.$matrix.getRoom(this.roomId);
|
||||||
|
if (
|
||||||
|
this.timelineWindow &&
|
||||||
|
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
|
||||||
|
) {
|
||||||
|
this.timelineWindow
|
||||||
|
.paginate(EventTimeline.BACKWARDS, 10, true)
|
||||||
|
.then((success) => {
|
||||||
|
if (success) {
|
||||||
|
this.scrollPosition.prepareFor("up");
|
||||||
|
this.events = this.timelineWindow.getEvents();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// restore scroll position!
|
||||||
|
console.log("Restore scroll!");
|
||||||
|
this.scrollPosition.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScrolledToBottom() {
|
||||||
|
console.log("@bottom");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<v-list flat>
|
<v-list dense>
|
||||||
<v-subheader>ROOMS</v-subheader>
|
<v-subheader>ROOMS</v-subheader>
|
||||||
<v-list-item-group v-model="currentRoomId" color="primary">
|
<v-list-item-group v-model="currentRoomId" color="primary">
|
||||||
<v-list-item v-for="room in $matrix.rooms" :key="room.roomId" :value="room.roomId">
|
<v-list-item v-for="room in $matrix.rooms" :key="room.roomId" :value="room.roomId">
|
||||||
<v-list-item-icon>
|
<v-list-item-avatar>
|
||||||
<v-icon v-text="room.icon"></v-icon>
|
<v-img :src="room.avatar" />
|
||||||
</v-list-item-icon>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title>{{ room.summary.info.title }}</v-list-item-title>
|
<v-list-item-title>{{ room.summary.info.title }}</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@ export default {
|
||||||
store: matrixStore,
|
store: matrixStore,
|
||||||
sessionStore: webStorageSessionStore,
|
sessionStore: webStorageSessionStore,
|
||||||
deviceId: user.device_id,
|
deviceId: user.device_id,
|
||||||
accessToken: user.access_token
|
accessToken: user.access_token,
|
||||||
|
timelineSupport: true
|
||||||
}
|
}
|
||||||
this.matrixClient = sdk.createClient(opts);
|
this.matrixClient = sdk.createClient(opts);
|
||||||
return this.matrixClient
|
return this.matrixClient
|
||||||
|
|
@ -147,17 +148,30 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
if (event.getType() == "m.room.topic") {
|
switch (event.getType()) {
|
||||||
const room = this.matrixClient.getRoom(event.getRoomId());
|
case "m.room.topic": {
|
||||||
if (room) {
|
const room = this.matrixClient.getRoom(event.getRoomId());
|
||||||
Vue.set(room, "topic", event.getContent().topic);
|
if (room) {
|
||||||
|
Vue.set(room, "topic", event.getContent().topic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
break;
|
||||||
}
|
|
||||||
|
case "m.room.avatar": {
|
||||||
|
const room = this.matrixClient.getRoom(event.getRoomId());
|
||||||
|
if (room) {
|
||||||
|
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reloadRooms() {
|
reloadRooms() {
|
||||||
this.rooms = this.matrixClient.getVisibleRooms();
|
this.rooms = this.matrixClient.getVisibleRooms();
|
||||||
|
this.rooms.forEach(room => {
|
||||||
|
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setCurrentRoomId(roomId) {
|
setCurrentRoomId(roomId) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue