Start rewriting backend with pyotherside+asyncio

This commit is contained in:
miruka
2019-06-27 02:31:03 -04:00
parent f530f51937
commit 3344debbbf
128 changed files with 715 additions and 2941 deletions

View File

@@ -0,0 +1,90 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
HRectangle {
id: banner
Layout.fillWidth: true
Layout.preferredHeight: HStyle.bottomElementsHeight
property alias avatar: bannerAvatar
property alias icon: bannerIcon
property alias labelText: bannerLabel.text
property alias buttonModel: bannerRepeater.model
property var buttonCallbacks: []
HRowLayout {
id: bannerRow
anchors.fill: parent
HAvatar {
id: bannerAvatar
dimension: banner.Layout.preferredHeight
}
HIcon {
id: bannerIcon
dimension: bannerLabel.implicitHeight
visible: Boolean(svgName)
Layout.leftMargin: 5
}
HLabel {
id: bannerLabel
textFormat: Text.StyledText
maximumLineCount: 1
elide: Text.ElideRight
visible:
bannerRow.width - bannerAvatar.width - bannerButtons.width > 30
Layout.maximumWidth:
bannerRow.width -
bannerAvatar.width - bannerButtons.width -
Layout.leftMargin - Layout.rightMargin
Layout.leftMargin: 5
Layout.rightMargin: Layout.leftMargin
}
HSpacer {}
HRowLayout {
id: bannerButtons
function getButtonsWidth() {
var total = 0
for (var i = 0; i < bannerRepeater.count; i++) {
total += bannerRepeater.itemAt(i).implicitWidth
}
return total
}
property bool compact:
bannerRow.width <
bannerAvatar.width +
bannerLabel.implicitWidth +
bannerLabel.Layout.leftMargin +
bannerLabel.Layout.rightMargin +
getButtonsWidth()
Repeater {
id: bannerRepeater
model: []
HButton {
id: button
text: modelData.text
iconName: modelData.iconName
onClicked: buttonCallbacks[modelData.name](button)
Layout.maximumWidth: bannerButtons.compact ? height : -1
Layout.fillHeight: true
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
import QtQuick 2.7
import "../../Base"
Banner {
property var inviter: null
color: HStyle.chat.inviteBanner.background
avatar.name: inviter ? inviter.displayname : ""
//avatar.imageUrl: inviter ? inviter.avatar_url : ""
labelText:
(inviter ?
("<b>" + inviter.displayname + "</b>") : qsTr("Someone")) +
" " + qsTr("invited you to join the room.")
buttonModel: [
{
name: "accept",
text: qsTr("Accept"),
iconName: "invite_accept",
},
{
name: "decline",
text: qsTr("Decline"),
iconName: "invite_decline",
}
]
buttonCallbacks: {
"accept": function(button) {
button.loading = true
Backend.clients.get(chatPage.userId).joinRoom(chatPage.roomId)
},
"decline": function(button) {
button.loading = true
Backend.clients.get(chatPage.userId).leaveRoom(chatPage.roomId)
}
}
}

View File

@@ -0,0 +1,28 @@
import QtQuick 2.7
import "../../Base"
import "../utils.js" as ChatJS
Banner {
property var leftEvent: null
color: HStyle.chat.leftBanner.background
avatar.name: ChatJS.getLeftBannerAvatarName(leftEvent, chatPage.userId)
labelText: ChatJS.getLeftBannerText(leftEvent)
buttonModel: [
{
name: "forget",
text: qsTr("Forget"),
iconName: "forget_room",
}
]
buttonCallbacks: {
"forget": function(button) {
button.loading = true
Backend.clients.get(chatPage.userId).forgetRoom(chatPage.roomId)
pageStack.clear()
},
}
}

View File

@@ -0,0 +1,25 @@
import QtQuick 2.7
import "../../Base"
import "../utils.js" as ChatJS
Banner {
color: HStyle.chat.unknownDevices.background
avatar.visible: false
icon.svgName: "unknown_devices_warning"
labelText: "Unknown devices are present in this encrypted room."
buttonModel: [
{
name: "inspect",
text: qsTr("Inspect"),
iconName: "unknown_devices_inspect",
}
]
buttonCallbacks: {
"inspect": function(button) {
print("show")
},
}
}

148
src/qml/Chat/Chat.qml Normal file
View File

@@ -0,0 +1,148 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
import "Banners"
import "RoomEventList"
import "RoomSidePane"
HColumnLayout {
property string userId: ""
property string category: ""
property string roomId: ""
readonly property var roomInfo:
Backend.accounts.get(userId)
.roomCategories.get(category)
.rooms.get(roomId)
readonly property var sender: Backend.users.get(userId)
readonly property bool hasUnknownDevices:
category == "Rooms" ?
Backend.clients.get(userId).roomHasUnknownDevices(roomId) : false
id: chatPage
onFocusChanged: sendBox.setFocus()
Component.onCompleted: Backend.signals.roomCategoryChanged.connect(
function(forUserId, forRoomId, previous, now) {
if (chatPage && forUserId == userId && forRoomId == roomId) {
chatPage.category = now
}
}
)
RoomHeader {
id: roomHeader
displayName: roomInfo.displayName
topic: roomInfo.topic || ""
Layout.fillWidth: true
Layout.preferredHeight: HStyle.avatar.size
}
HSplitView {
id: chatSplitView
Layout.fillWidth: true
Layout.fillHeight: true
HColumnLayout {
Layout.fillWidth: true
RoomEventList {
Layout.fillWidth: true
Layout.fillHeight: true
}
TypingMembersBar {}
InviteBanner {
visible: category === "Invites"
inviter: roomInfo.inviter
}
UnknownDevicesBanner {
visible: category == "Rooms" && hasUnknownDevices
}
SendBox {
id: sendBox
visible: category == "Rooms" && ! hasUnknownDevices
}
LeftBanner {
visible: category === "Left"
leftEvent: roomInfo.leftEvent
}
}
RoomSidePane {
id: roomSidePane
activeView: roomHeader.activeButton
property int oldWidth: width
onActiveViewChanged:
activeView ? restoreAnimation.start() : hideAnimation.start()
NumberAnimation {
id: hideAnimation
target: roomSidePane
properties: "width"
duration: HStyle.animationDuration
from: target.width
to: 0
onStarted: {
target.oldWidth = target.width
target.Layout.minimumWidth = 0
}
}
NumberAnimation {
id: restoreAnimation
target: roomSidePane
properties: "width"
duration: HStyle.animationDuration
from: 0
to: target.oldWidth
onStopped: target.Layout.minimumWidth = Qt.binding(
function() { return HStyle.avatar.size }
)
}
collapsed: width < HStyle.avatar.size + 8
property bool wasSnapped: false
property int referenceWidth: roomHeader.buttonsWidth
onReferenceWidthChanged: {
if (chatSplitView.canAutoSize || wasSnapped) {
if (wasSnapped) { chatSplitView.canAutoSize = true }
width = referenceWidth
}
}
property int currentWidth: width
onCurrentWidthChanged: {
if (referenceWidth != width &&
referenceWidth - 15 < width &&
width < referenceWidth + 15)
{
currentWidth = referenceWidth
width = referenceWidth
wasSnapped = true
currentWidth = Qt.binding(
function() { return roomSidePane.width }
)
} else {
wasSnapped = false
}
}
width: referenceWidth // Initial width
Layout.minimumWidth: HStyle.avatar.size
Layout.maximumWidth: parent.width
}
}
}

View File

@@ -0,0 +1,9 @@
import QtQuick 2.7
import "../../Base"
HNoticePage {
text: dateTime.toLocaleDateString()
color: HStyle.chat.daybreak.foreground
backgroundColor: HStyle.chat.daybreak.background
radius: HStyle.chat.daybreak.radius
}

View File

@@ -0,0 +1,53 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
import "../utils.js" as ChatJS
Row {
id: eventContent
spacing: standardSpacing / 2
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
width: Math.min(
roomEventListView.width - avatar.width - eventContent.spacing,
HStyle.fontSize.normal * 0.5 * 75, // 600 with 16px font
contentLabel.implicitWidth
)
HAvatar {
id: avatar
name: sender.displayName.value
hidden: combine
dimension: 28
}
HLabel {
width: parent.width
id: contentLabel
text: "<font color='" +
Qt.hsla(Backend.hueFromString(sender.displayName.value),
HStyle.chat.event.saturation,
HStyle.chat.event.lightness,
1) +
"'>" +
sender.displayName.value + " " +
ChatJS.getEventText(type, dict) +
"&nbsp;&nbsp;" +
"<font size=" + HStyle.fontSize.small + "px " +
"color=" + HStyle.chat.event.date + ">" +
Qt.formatDateTime(dateTime, "hh:mm:ss") +
"</font> " +
"</font>"
textFormat: Text.RichText
background: Rectangle {color: HStyle.chat.event.background}
wrapMode: Text.Wrap
leftPadding: horizontalPadding
rightPadding: horizontalPadding
topPadding: verticalPadding
bottomPadding: verticalPadding
}
}

View File

@@ -0,0 +1,80 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
Row {
id: messageContent
spacing: standardSpacing / 2
layoutDirection: isOwn ? Qt.RightToLeft : Qt.LeftToRight
HAvatar {
id: avatar
hidden: combine
name: sender.displayName.value
dimension: 48
}
Rectangle {
color: HStyle.chat.message.background
//width: nameLabel.implicitWidth
width: Math.min(
roomEventListView.width - avatar.width - messageContent.spacing,
HStyle.fontSize.normal * 0.5 * 75, // 600 with 16px font
Math.max(
nameLabel.visible ? nameLabel.implicitWidth : 0,
contentLabel.implicitWidth
)
)
height: nameLabel.height + contentLabel.implicitHeight
Column {
spacing: 0
anchors.fill: parent
HLabel {
height: combine ? 0 : implicitHeight
width: parent.width
visible: height > 0
id: nameLabel
text: sender.displayName.value
color: Qt.hsla(Backend.hueFromString(text),
HStyle.displayName.saturation,
HStyle.displayName.lightness,
1)
elide: Text.ElideRight
maximumLineCount: 1
horizontalAlignment: isOwn ? Text.AlignRight : Text.AlignLeft
leftPadding: horizontalPadding
rightPadding: horizontalPadding
topPadding: verticalPadding
}
HRichLabel {
width: parent.width
id: contentLabel
text: (dict.formatted_body ?
Backend.htmlFilter.filter(dict.formatted_body) :
dict.body) +
"&nbsp;&nbsp;<font size=" + HStyle.fontSize.small +
"px color=" + HStyle.chat.message.date + ">" +
Qt.formatDateTime(dateTime, "hh:mm:ss") +
"</font>" +
(isLocalEcho ?
"&nbsp;<font size=" + HStyle.fontSize.small +
"px>⏳</font>" : "")
textFormat: Text.RichText
color: HStyle.chat.message.body
wrapMode: Text.Wrap
leftPadding: horizontalPadding
rightPadding: horizontalPadding
topPadding: nameLabel.visible ? 0 : verticalPadding
bottomPadding: verticalPadding
}
}
}
}

View File

@@ -0,0 +1,88 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
import "../utils.js" as ChatJS
Column {
id: roomEventDelegate
function minsBetween(date1, date2) {
return Math.round((((date2 - date1) % 86400000) % 3600000) / 60000)
}
function getIsMessage(type_) { return type_.startsWith("RoomMessage") }
function getPreviousItem() {
return index < roomEventListView.model.count - 1 ?
roomEventListView.model.get(index + 1) : null
}
property var previousItem: getPreviousItem()
signal reloadPreviousItem()
onReloadPreviousItem: previousItem = getPreviousItem()
readonly property bool isMessage: getIsMessage(type)
readonly property bool isUndecryptableEvent:
type === "OlmEvent" || type === "MegolmEvent"
readonly property var sender: Backend.users.get(dict.sender)
readonly property bool isOwn:
chatPage.userId === dict.sender
readonly property bool isFirstEvent: type == "RoomCreateEvent"
readonly property bool combine:
previousItem &&
! talkBreak &&
! dayBreak &&
getIsMessage(previousItem.type) === isMessage &&
previousItem.dict.sender === dict.sender &&
minsBetween(previousItem.dateTime, dateTime) <= 5
readonly property bool dayBreak:
isFirstEvent ||
previousItem &&
dateTime.getDate() != previousItem.dateTime.getDate()
readonly property bool talkBreak:
previousItem &&
! dayBreak &&
minsBetween(previousItem.dateTime, dateTime) >= 20
property int standardSpacing: 16
property int horizontalPadding: 6
property int verticalPadding: 4
ListView.onAdd: {
var nextDelegate = roomEventListView.contentItem.children[index]
if (nextDelegate) { nextDelegate.reloadPreviousItem() }
}
width: parent.width
topPadding:
isFirstEvent ? 0 :
dayBreak ? standardSpacing * 2 :
talkBreak ? standardSpacing * 3 :
combine ? standardSpacing / 4 :
standardSpacing
Loader {
source: dayBreak ? "Daybreak.qml" : ""
width: roomEventDelegate.width
}
Item {
visible: dayBreak
width: parent.width
height: topPadding
}
Loader {
source: isMessage ? "MessageContent.qml" : "EventContent.qml"
anchors.right: isOwn ? parent.right : undefined
}
}

View File

@@ -0,0 +1,43 @@
import QtQuick 2.7
import "../../Base"
HRectangle {
property int space: 8
color: HStyle.chat.roomEventList.background
HListView {
id: roomEventListView
delegate: RoomEventDelegate {}
model: Backend.roomEvents.get(chatPage.roomId)
clip: true
anchors.fill: parent
anchors.leftMargin: space
anchors.rightMargin: space
topMargin: space
bottomMargin: space
verticalLayoutDirection: ListView.BottomToTop
// Keep x scroll pages cached, to limit images having to be
// reloaded from network.
cacheBuffer: height * 6
// Declaring this "alias" provides the on... signal
property real yPos: visibleArea.yPosition
onYPosChanged: {
if (chatPage.category != "Invites" && yPos <= 0.1) {
Backend.loadPastEvents(chatPage.roomId)
}
}
}
HNoticePage {
text: qsTr("Nothing to show here yet...")
visible: roomEventListView.model.count < 1
anchors.fill: parent
}
}

102
src/qml/Chat/RoomHeader.qml Normal file
View File

@@ -0,0 +1,102 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HRectangle {
property string displayName: ""
property string topic: ""
property alias buttonsImplicitWidth: viewButtons.implicitWidth
property int buttonsWidth: viewButtons.Layout.preferredWidth
property var activeButton: "members"
property bool collapseButtons: width < 400
id: roomHeader
color: HStyle.chat.roomHeader.background
HRowLayout {
id: row
spacing: 8
anchors.fill: parent
HAvatar {
id: avatar
name: displayName
dimension: roomHeader.height
Layout.alignment: Qt.AlignTop
}
HLabel {
id: roomName
text: displayName
font.pixelSize: HStyle.fontSize.big
elide: Text.ElideRight
maximumLineCount: 1
Layout.maximumWidth: Math.max(
0,
row.width - row.totalSpacing - avatar.width -
viewButtons.width -
(expandButton.visible ? expandButton.width : 0)
)
}
HLabel {
id: roomTopic
text: topic
font.pixelSize: HStyle.fontSize.small
elide: Text.ElideRight
maximumLineCount: 1
Layout.maximumWidth: Math.max(
0,
row.width - row.totalSpacing - avatar.width -
roomName.width - viewButtons.width -
(expandButton.visible ? expandButton.width : 0)
)
}
HSpacer {}
Row {
id: viewButtons
Layout.preferredWidth: collapseButtons ? 0 : implicitWidth
Layout.fillHeight: true
Repeater {
model: [
"members", "files", "notifications", "history", "settings"
]
HButton {
iconName: "room_view_" + modelData
iconDimension: 22
autoExclusive: true
checked: activeButton == modelData
onClicked: activeButton = activeButton == modelData ?
null : modelData
}
}
Behavior on Layout.preferredWidth {
NumberAnimation {
id: buttonsAnimation
duration: HStyle.animationDuration
}
}
}
}
HButton {
id: expandButton
z: 1
anchors.right: parent.right
opacity: collapseButtons ? 1 : 0
visible: opacity > 0
iconName: "reduced_room_buttons"
Behavior on opacity {
NumberAnimation { duration: buttonsAnimation.duration * 2 }
}
}
}

View File

@@ -0,0 +1,37 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
MouseArea {
id: memberDelegate
width: memberList.width
height: childrenRect.height
property var member: Backend.users.get(userId)
HRowLayout {
width: parent.width
spacing: memberList.spacing
HAvatar {
id: memberAvatar
name: member.displayName.value
}
HColumnLayout {
Layout.fillWidth: true
Layout.maximumWidth:
parent.width - parent.totalSpacing - memberAvatar.width
HLabel {
id: memberName
text: member.displayName.value
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Qt.AlignVCenter
Layout.maximumWidth: parent.width
}
}
}
}

View File

@@ -0,0 +1,49 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
HColumnLayout {
property bool collapsed: false
property int normalSpacing: collapsed ? 0 : 8
Behavior on normalSpacing {
NumberAnimation { duration: HStyle.animationDuration }
}
HListView {
id: memberList
spacing: normalSpacing
topMargin: normalSpacing
bottomMargin: normalSpacing
Layout.leftMargin: normalSpacing
Layout.rightMargin: normalSpacing
model: chatPage.roomInfo.sortedMembers
delegate: MemberDelegate {}
Layout.fillWidth: true
Layout.fillHeight: true
}
HTextField {
id: filterField
placeholderText: qsTr("Filter members")
backgroundColor: HStyle.sidePane.filterRooms.background
// Without this, if the user types in the field, changes of room, then
// comes back, the field will be empty but the filter still applied.
Component.onCompleted:
text = Backend.clients.get(chatPage.userId).getMemberFilter(
chatPage.category, chatPage.roomId
)
onTextChanged: Backend.clients.get(chatPage.userId).setMemberFilter(
chatPage.category, chatPage.roomId, text
)
Layout.fillWidth: true
Layout.preferredHeight: HStyle.bottomElementsHeight
}
}

View File

@@ -0,0 +1,15 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../../Base"
HRectangle {
id: roomSidePane
property bool collapsed: false
property var activeView: null
MembersView {
anchors.fill: parent
collapsed: parent.collapsed
}
}

72
src/qml/Chat/SendBox.qml Normal file
View File

@@ -0,0 +1,72 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
HRectangle {
function setFocus() { textArea.forceActiveFocus() }
id: root
Layout.fillWidth: true
Layout.minimumHeight: HStyle.bottomElementsHeight
Layout.preferredHeight: textArea.implicitHeight
// parent.height / 2 causes binding loop?
Layout.maximumHeight: pageStack.height / 2
color: HStyle.chat.sendBox.background
HRowLayout {
anchors.fill: parent
HAvatar {
id: avatar
name: chatPage.sender.displayName.value
dimension: root.Layout.minimumHeight
}
HScrollableTextArea {
Layout.fillHeight: true
Layout.fillWidth: true
id: textArea
placeholderText: qsTr("Type a message...")
backgroundColor: "transparent"
area.focus: true
property bool textChangedSinceLostFocus: false
function setTyping(typing) {
Backend.clients.get(chatPage.userId)
.setTypingState(chatPage.roomId, typing)
}
onTextChanged: {
setTyping(Boolean(text))
textChangedSinceLostFocus = true
}
area.onEditingFinished: { // when lost focus
if (text && textChangedSinceLostFocus) {
setTyping(false)
textChangedSinceLostFocus = false
}
}
Keys.onReturnPressed: {
event.accepted = true
if (event.modifiers & Qt.ShiftModifier ||
event.modifiers & Qt.ControlModifier ||
event.modifiers & Qt.AltModifier) {
textArea.insert(textArea.cursorPosition, "\n")
return
}
if (textArea.text === "") { return }
Backend.clients.get(chatPage.userId)
.sendMarkdown(chatPage.roomId, textArea.text)
area.clear()
}
// Numpad enter
Keys.onEnterPressed: Keys.onReturnPressed(event)
}
}
}

View File

@@ -0,0 +1,23 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import "../Base"
import "utils.js" as ChatJS
HRectangle {
property var typingMembers: chatPage.roomInfo.typingMembers
color: HStyle.chat.typingMembers.background
Layout.fillWidth: true
Layout.minimumHeight: usersLabel.text ? usersLabel.implicitHeight : 0
Layout.maximumHeight: Layout.minimumHeight
HLabel {
id: usersLabel
anchors.fill: parent
text: ChatJS.getTypingMembersText(typingMembers, chatPage.userId)
elide: Text.ElideMiddle
maximumLineCount: 1
}
}

210
src/qml/Chat/utils.js Normal file
View File

@@ -0,0 +1,210 @@
function getEventText(type, dict) {
switch (type) {
case "RoomCreateEvent":
return (dict.federate ? "allowed" : "blocked") +
" users on other matrix servers " +
(dict.federate ? "to join" : "from joining") +
" this room."
break
case "RoomGuestAccessEvent":
return (dict.guest_access === "can_join" ? "allowed " : "forbad") +
"guests to join the room."
break
case "RoomJoinRulesEvent":
return "made the room " +
(dict.join_rule === "public." ? "public" : "invite only.")
break
case "RoomHistoryVisibilityEvent":
return getHistoryVisibilityEventText(dict)
break
case "PowerLevelsEvent":
return "changed the room's permissions."
case "RoomMemberEvent":
return getMemberEventText(dict)
break
case "RoomAliasEvent":
return "set the room's main address to " +
dict.canonical_alias + "."
break
case "RoomNameEvent":
return "changed the room's name to \"" + dict.name + "\"."
break
case "RoomTopicEvent":
return "changed the room's topic to \"" + dict.topic + "\"."
break
case "RoomEncryptionEvent":
return "turned on encryption for this room."
break
case "OlmEvent":
case "MegolmEvent":
return "hasn't sent your device the keys to decrypt this message."
default:
console.log(type + "\n" + JSON.stringify(dict, null, 4) + "\n")
return "did something this client does not understand."
//case "CallEvent": TODO
}
}
function getHistoryVisibilityEventText(dict) {
switch (dict.history_visibility) {
case "shared":
var end = "all room members."
break
case "world_readable":
var end = "any member or outsider."
break
case "joined":
var end = "all room members, since the point they joined."
break
case "invited":
var end = "all room members, since the point they were invited."
break
}
return "made future history visible to " + end
}
function getStateDisplayName(dict) {
// The dict.content.displayname may be outdated, prefer
// retrieving it fresh
return Backend.users.get(dict.state_key).displayName.value
}
function getMemberEventText(dict) {
var info = dict.content, prev = dict.prev_content
if (! prev || (info.membership != prev.membership)) {
var reason = info.reason ? (" Reason: " + info.reason) : ""
switch (info.membership) {
case "join":
return prev && prev.membership === "invite" ?
"accepted the invitation." : "joined the room."
break
case "invite":
return "invited " + getStateDisplayName(dict) + " to the room."
break
case "leave":
if (dict.state_key === dict.sender) {
return (prev && prev.membership === "invite" ?
"declined the invitation." : "left the room.") +
reason
}
var name = getStateDisplayName(dict)
return (prev && prev.membership === "invite" ?
"withdrew " + name + "'s invitation." :
prev && prev.membership == "ban" ?
"unbanned " + name + " from the room." :
"kicked out " + name + " from the room.") +
reason
break
case "ban":
var name = getStateDisplayName(dict)
return "banned " + name + " from the room." + reason
break
}
}
var changed = []
if (prev && (info.avatar_url != prev.avatar_url)) {
changed.push("profile picture")
}
if (prev && (info.displayname != prev.displayname)) {
changed.push("display name from \"" +
(prev.displayname || dict.state_key) + '" to "' +
(info.displayname || dict.state_key) + '"')
}
if (changed.length > 0) {
return "changed their " + changed.join(" and ") + "."
}
return ""
}
function getLeftBannerText(leftEvent) {
if (! leftEvent) {
return "You are not member of this room."
}
var info = leftEvent.content
var prev = leftEvent.prev_content
var reason = info.reason ? (" Reason: " + info.reason) : ""
if (leftEvent.state_key === leftEvent.sender) {
return (prev && prev.membership === "invite" ?
"You declined to join the room." : "You left the room.") +
reason
}
if (info.membership)
var name = Backend.users.get(leftEvent.sender).displayName.value
return "<b>" + name + "</b> " +
(info.membership == "ban" ?
"banned you from the room." :
prev && prev.membership === "invite" ?
"canceled your invitation." :
prev && prev.membership == "ban" ?
"unbanned you from the room." :
"kicked you out of the room.") +
reason
}
function getLeftBannerAvatarName(leftEvent, accountId) {
if (! leftEvent || leftEvent.state_key == leftEvent.sender) {
return Backend.users.get(accountId).displayName.value
}
return Backend.users.get(leftEvent.sender).displayName.value
}
function getTypingMembersText(users, ourAccountId) {
var names = []
for (var i = 0; i < users.length; i++) {
if (users[i] !== ourAccountId) {
names.push(Backend.users.get(users[i]).displayName.value)
}
}
if (names.length < 1) { return "" }
return "🖋 " +
[names.slice(0, -1).join(", "), names.slice(-1)[0]]
.join(names.length < 2 ? "" : " and ") +
(names.length > 1 ? " are" : " is") + " typing…"
}