feat: user list

This commit is contained in:
Richard Ramos 2021-06-30 14:46:26 -04:00 committed by Iuri Matias
parent 189d761d20
commit 2fcbe4ac16
10 changed files with 637 additions and 293 deletions

View File

@ -7,6 +7,7 @@ import ../../../status/profile/profile
import ../../../status/ens
import strutils
import message_format
import user_list
type
ChatMessageRoles {.pure.} = enum
@ -55,6 +56,7 @@ QtObject:
isEdited*: Table[string, bool]
messageReactions*: Table[string, string]
timedoutMessages: HashSet[string]
userList: UserListView
proc delete(self: ChatMessageList) =
self.messages = @[]
@ -90,6 +92,7 @@ QtObject:
result.messageIndex = initTable[string, int]()
result.timedoutMessages = initHashSet[string]()
result.isEdited = initTable[string, bool]()
result.userList = newUserListView(status)
result.status = status
result.setup
@ -292,6 +295,7 @@ QtObject:
self.beginInsertRows(newQModelIndex(), self.messages.len, self.messages.len)
self.messageIndex[message.id] = self.messages.len
self.messages.add(message)
self.userList.add(message)
self.countChanged()
self.endInsertRows()
@ -301,6 +305,7 @@ QtObject:
if self.messageIndex.hasKey(message.id): continue
self.messageIndex[message.id] = self.messages.len
self.messages.add(message)
self.userList.add(message)
self.countChanged()
self.endInsertRows()
@ -364,6 +369,7 @@ QtObject:
m.userName = userNameOrAlias(c)
m.alias = c.alias
m.localName = c.localNickname
self.userList.updateUsernames(c.id, m.username, m.alias, m.localname)
self.dataChanged(topLeft, bottomRight, @[ChatMessageRoles.Username.int])
@ -376,4 +382,10 @@ QtObject:
self.deleteMessage(m)
proc getID*(self: ChatMessageList):string {.slot.} =
self.id
self.id
proc getUserList*(self: ChatMessageList): QVariant {.slot.} =
newQVariant(self.userList)
QtProperty[QVariant] userList:
read = getUserList

View File

@ -0,0 +1,118 @@
import NimQml, Tables, json, chronicles, sequtils
import ../../../status/status
import ../../../status/accounts
import ../../../status/chat
import ../../../status/chat/[message]
import strutils
type
UserListRoles {.pure.} = enum
UserName = UserRole + 1
LastSeen = UserRole + 2
PublicKey = UserRole + 3
Alias = UserRole + 4
LocalName = UserRole + 5
Identicon = UserRole + 6
User = object
username: string
alias: string
localName: string
lastSeen: string
identicon: string
QtObject:
type
UserListView* = ref object of QAbstractListModel
status: Status
users: seq[string]
userDetails: OrderedTable[string, User]
proc delete(self: UserListView) =
self.userDetails.clear()
self.QAbstractListModel.delete
proc setup(self: UserListView) =
self.QAbstractListModel.setup
proc newUserListView*(status: Status): UserListView =
new(result, delete)
result.userDetails = initOrderedTable[string, User]()
result.users = @[]
result.status = status
result.setup
method rowCount*(self: UserListView, index: QModelIndex = nil): int = self.users.len
method data(self: UserListView, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.users.len:
return
let user = self.users[index.row]
case role.UserListRoles:
of UserListRoles.UserName: result = newQVariant(self.userDetails[user].userName)
of UserListRoles.LastSeen: result = newQVariant(self.userDetails[user].lastSeen)
of UserListRoles.Alias: result = newQVariant(self.userDetails[user].alias)
of UserListRoles.LocalName: result = newQVariant(self.userDetails[user].localName)
of UserListRoles.PublicKey: result = newQVariant(user)
of UserListRoles.Identicon: result = newQVariant(self.userdetails[user].identicon)
method roleNames(self: UserListView): Table[int, string] =
{
UserListRoles.UserName.int:"userName",
UserListRoles.LastSeen.int:"lastSeen",
UserListRoles.PublicKey.int:"publicKey",
UserListRoles.Alias.int:"alias",
UserListRoles.LocalName.int:"localName",
UserListRoles.Identicon.int:"identicon"
}.toTable
proc add*(self: UserListView, message: Message) =
if self.userDetails.hasKey(message.fromAuthor):
self.beginResetModel()
self.userDetails[message.fromAuthor] = User(
userName: message.userName,
alias: message.alias,
localName: message.localName,
lastSeen: message.timestamp,
identicon: message.identicon
)
self.endResetModel()
else:
self.beginInsertRows(newQModelIndex(), self.users.len, self.users.len)
self.userDetails[message.fromAuthor] = User(
userName: message.userName,
alias: message.alias,
localName: message.localName,
lastSeen: message.timestamp,
identicon: message.identicon
)
self.users.add(message.fromAuthor)
self.endInsertRows()
proc triggerUpdate*(self: UserListView) {.slot.} =
self.beginResetModel()
self.endResetModel()
proc updateUsernames*(self: UserListView, publicKey, userName, alias, localName: string) =
if not self.userDetails.hasKey(publicKey): return
var i = -1
var found = -1
for u in self.users:
i = i + 1
if u == publicKey:
found = i
if found == -1: return
self.userDetails[publicKey].username = userName
self.userDetails[publicKey].alias = alias
self.userDetails[publicKey].localName = localName
let topLeft = self.createIndex(found, 0, nil)
let bottomRight = self.createIndex(found, 0, nil)
self.dataChanged(topLeft, bottomRight, @[UserListRoles.Username.int, UserListRoles.Alias.int, UserListRoles.Localname.int])

View File

@ -25,6 +25,8 @@ StackLayout {
property bool isConnected: false
property string contactToRemove: ""
property bool showUsers: false
property var doNotShowAddToContactBannerToThose: ([])
@ -64,6 +66,17 @@ StackLayout {
chatInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
property var currentTime: 0
Timer {
interval: 60000; // 1 min
running: true;
repeat: true
onTriggered: {
chatColumnLayout.currentTime = Date.now()
}
}
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumWidth: 300
@ -266,6 +279,7 @@ StackLayout {
sourceComponent: ChatMessages {
id: chatMessages
messageList: model.messages
currentTime: chatColumnLayout.currentTime
}
}
}

View File

@ -12,10 +12,10 @@ import "../../../../imports"
import "../components"
import "./samples/"
import "./MessageComponents"
import "../ContactsColumn"
ScrollView {
id: root
SplitView {
id: svRoot
property alias chatLogView: chatLogView
property alias scrollToMessage: chatLogView.scrollToMessage
@ -23,337 +23,387 @@ ScrollView {
property bool loadingMessages: false
property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
property int newMessages: 0
property var currentTime
contentItem: chatLogView
Layout.fillWidth: true
Layout.fillHeight: true
height: parent.height
ScrollBar.vertical.policy: chatLogView.contentHeight > chatLogView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
handle: SplitViewHandle { implicitWidth: 5}
ListView {
id: chatLogView
anchors.fill: parent
anchors.bottomMargin: Style.current.bigPadding
spacing: appSettings.useCompactMode ? 0 : 4
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: {
if (utilsModel.getOs() === Constants.windows) {
return 5000
ScrollView {
id: root
contentItem: chatLogView
SplitView.fillWidth: true
SplitView.minimumWidth: 200
ScrollBar.vertical.policy: chatLogView.contentHeight > chatLogView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: chatLogView
anchors.fill: parent
anchors.bottomMargin: Style.current.bigPadding
spacing: appSettings.useCompactMode ? 0 : 4
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: {
if (utilsModel.getOs() === Constants.windows) {
return 5000
}
return 10000
}
return 10000
}
verticalLayoutDirection: ListView.BottomToTop
verticalLayoutDirection: ListView.BottomToTop
// This header and Connections is to create an invisible padding so that the chat identifier is at the top
// The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height
// If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections
header: Item {
height: 0
width: chatLogView.width
}
function checkHeaderHeight() {
if (!chatLogView.headerItem) {
return
// This header and Connections is to create an invisible padding so that the chat identifier is at the top
// The Connections is necessary, because doing the check inside the header created a binding loop (the contentHeight includes the header height
// If the content height is smaller than the full height, we "show" the padding so that the chat identifier is at the top, otherwise we disable the Connections
header: Item {
height: 0
width: chatLogView.width
}
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
property var scrollToMessage: function (messageId) {
let item
for (let i = 0; i < messageListDelegate.count; i++) {
item = messageListDelegate.items.get(i)
if (item.model.messageId === messageId) {
chatLogView.positionViewAtIndex(i, ListView.Center)
function checkHeaderHeight() {
if (!chatLogView.headerItem) {
return
}
}
}
Connections {
id: contentHeightConnection
enabled: true
target: chatLogView
onContentHeightChanged: {
chatLogView.checkHeaderHeight()
}
onHeightChanged: {
chatLogView.checkHeaderHeight()
}
}
Timer {
id: timer
}
Button {
readonly property int buttonPadding: 5
id: scrollDownButton
visible: false
height: 32
width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0)
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
background: Rectangle {
color: Style.current.buttonSecondaryColor
border.width: 0
radius: 16
}
onClicked: {
root.newMessages = 0
scrollDownButton.visible = false
chatLogView.scrollToBottom(true)
}
StyledText {
id: nbMessages
visible: root.newMessages > 0
width: visible ? implicitWidth : 0
text: root.newMessages
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
color: Style.current.pillButtonTextColor
font.pixelSize: 15
anchors.leftMargin: Style.current.halfPadding
}
SVGImage {
id: arrowImage
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
anchors.left: nbMessages.right
source: "../../../img/leave_chat.svg"
anchors.leftMargin: nbMessages.visible ? scrollDownButton.buttonPadding : 0
rotation: -90
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.pillButtonTextColor
if (chatLogView.contentItem.height - chatLogView.headerItem.height < chatLogView.height) {
chatLogView.headerItem.height = chatLogView.height - (chatLogView.contentItem.height - chatLogView.headerItem.height) - 36
} else {
chatLogView.headerItem.height = 0
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}
function scrollToBottom(force, caller) {
if (!force && !chatLogView.atYEnd) {
// User has scrolled up, we don't want to scroll back
return false
}
if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) {
// If we have a caller, only accept its request if it's the last message
return false
}
// Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads
// meaning that the scroll will not actually be at the bottom on switch
// Add a small delay because images, even though they say they say they are loaed, they aren't shown yet
Qt.callLater(chatLogView.positionViewAtBeginning)
timer.setTimeout(function() {
Qt.callLater(chatLogView.positionViewAtBeginning)
}, 100);
return true
}
Connections {
target: chatsModel
onAppReady: {
chatLogView.scrollToBottom(true)
}
}
Connections {
target: chatsModel.messageView
onMessagesLoaded: {
loadingMessages = false;
}
onSendingMessage: {
chatLogView.scrollToBottom(true)
}
onSendingMessageFailed: {
sendingMsgFailedPopup.open();
}
onNewMessagePushed: {
if (!chatLogView.scrollToBottom()) {
root.newMessages++
}
}
onMessageNotificationPushed: function(chatId, msg, messageType, chatType, timestamp, identicon, username, hasMention, isAddedContact, channelName) {
if (messageType == Constants.editType) return;
if (appSettings.notificationSetting == Constants.notifyAllMessages ||
(appSettings.notificationSetting == Constants.notifyJustMentions && hasMention)) {
if (chatId === chatsModel.channelView.activeChannel.id && applicationWindow.active === true) {
// Do not show the notif if we are in the channel already and the window is active and focused
property var scrollToMessage: function (messageId) {
let item
for (let i = 0; i < messageListDelegate.count; i++) {
item = messageListDelegate.items.get(i)
if (item.model.messageId === messageId) {
chatLogView.positionViewAtIndex(i, ListView.Center)
return
}
}
}
chatColumnLayout.currentNotificationChatId = chatId
chatColumnLayout.currentNotificationCommunityId = null
Connections {
id: contentHeightConnection
enabled: true
target: chatLogView
onContentHeightChanged: {
chatLogView.checkHeaderHeight()
}
onHeightChanged: {
chatLogView.checkHeaderHeight()
}
}
let name;
if (appSettings.notificationMessagePreviewSetting === Constants.notificationPreviewAnonymous) {
name = "Status"
} else if (chatType === Constants.chatTypePublic) {
name = chatId
} else {
name = chatType === Constants.chatTypePrivateGroupChat ? Utils.filterXSS(channelName) : Utils.removeStatusEns(username)
Timer {
id: timer
}
Button {
readonly property int buttonPadding: 5
id: scrollDownButton
visible: false
height: 32
width: nbMessages.width + arrowImage.width + 2 * Style.current.halfPadding + (nbMessages.visible ? scrollDownButton.buttonPadding : 0)
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
background: Rectangle {
color: Style.current.buttonSecondaryColor
border.width: 0
radius: 16
}
onClicked: {
root.newMessages = 0
scrollDownButton.visible = false
chatLogView.scrollToBottom(true)
}
StyledText {
id: nbMessages
visible: root.newMessages > 0
width: visible ? implicitWidth : 0
text: root.newMessages
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
color: Style.current.pillButtonTextColor
font.pixelSize: 15
anchors.leftMargin: Style.current.halfPadding
}
SVGImage {
id: arrowImage
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
anchors.left: nbMessages.right
source: "../../../img/leave_chat.svg"
anchors.leftMargin: nbMessages.visible ? scrollDownButton.buttonPadding : 0
rotation: -90
ColorOverlay {
anchors.fill: parent
source: parent
color: Style.current.pillButtonTextColor
}
}
let message;
if (appSettings.notificationMessagePreviewSetting > Constants.notificationPreviewNameOnly) {
switch(messageType){
//% "Image"
case Constants.imageType: message = qsTrId("image"); break
//% "Sticker"
case Constants.stickerType: message = qsTrId("sticker"); break
default: message = msg // don't parse emojis here as it emits HTML
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onPressed: mouse.accepted = false
}
}
function scrollToBottom(force, caller) {
if (!force && !chatLogView.atYEnd) {
// User has scrolled up, we don't want to scroll back
return false
}
if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) {
// If we have a caller, only accept its request if it's the last message
return false
}
// Call this twice and with a timer since the first scroll to bottom might have happened before some stuff loads
// meaning that the scroll will not actually be at the bottom on switch
// Add a small delay because images, even though they say they say they are loaed, they aren't shown yet
Qt.callLater(chatLogView.positionViewAtBeginning)
timer.setTimeout(function() {
Qt.callLater(chatLogView.positionViewAtBeginning)
}, 100);
return true
}
Connections {
target: chatsModel.messageView
onMessagesLoaded: {
loadingMessages = false;
}
onSendingMessage: {
chatLogView.scrollToBottom(true)
}
onSendingMessageFailed: {
sendingMsgFailedPopup.open();
}
onNewMessagePushed: {
if (!chatLogView.scrollToBottom()) {
root.newMessages++
}
}
onAppReady: {
chatLogView.scrollToBottom(true)
}
onMessageNotificationPushed: function(chatId, msg, messageType, chatType, timestamp, identicon, username, hasMention, isAddedContact, channelName) {
if (messageType == Constants.editType) return;
if (appSettings.notificationSetting == Constants.notifyAllMessages ||
(appSettings.notificationSetting == Constants.notifyJustMentions && hasMention)) {
if (chatId === chatsModel.channelView.activeChannel.id && applicationWindow.active === true) {
// Do not show the notif if we are in the channel already and the window is active and focused
return
}
} else {
//% "You have a new message"
message = qsTrId("you-have-a-new-message")
}
currentlyHasANotification = true
if (appSettings.useOSNotifications && systemTray.supportsMessages) {
systemTray.showMessage(name,
message,
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
} else {
notificationWindow.notifyUser(chatId, name, message, chatType, identicon, chatColumnLayout.clickOnNotification)
chatColumnLayout.currentNotificationChatId = chatId
chatColumnLayout.currentNotificationCommunityId = null
let name;
if (appSettings.notificationMessagePreviewSetting === Constants.notificationPreviewAnonymous) {
name = "Status"
} else if (chatType === Constants.chatTypePublic) {
name = chatId
} else {
name = chatType === Constants.chatTypePrivateGroupChat ? Utils.filterXSS(channelName) : Utils.removeStatusEns(username)
}
let message;
if (appSettings.notificationMessagePreviewSetting > Constants.notificationPreviewNameOnly) {
switch(messageType){
//% "Image"
case Constants.imageType: message = qsTrId("image"); break
//% "Sticker"
case Constants.stickerType: message = qsTrId("sticker"); break
default: message = msg // don't parse emojis here as it emits HTML
}
} else {
//% "You have a new message"
message = qsTrId("you-have-a-new-message")
}
currentlyHasANotification = true
if (appSettings.useOSNotifications && systemTray.supportsMessages) {
systemTray.showMessage(name,
message,
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
} else {
notificationWindow.notifyUser(chatId, name, message, chatType, identicon, chatColumnLayout.clickOnNotification)
}
}
}
}
}
Connections {
target: chatsModel.communities
Connections {
target: chatsModel.communities
onMembershipRequestChanged: function (communityId, communityName, accepted) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage("Status",
accepted ? qsTr("You have been accepted into the %1 community").arg(communityName) :
qsTr("Your request to join the %1 community was declined").arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
onMembershipRequestChanged: function (communityId, communityName, accepted) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage("Status",
accepted ? qsTr("You have been accepted into the %1 community").arg(communityName) :
qsTr("Your request to join the %1 community was declined").arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
onMembershipRequestPushed: function (communityId, communityName, pubKey) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage(qsTr("New membership request"),
qsTr("%1 asks to join %2").arg(Utils.getDisplayName(pubKey)).arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
}
}
onMembershipRequestPushed: function (communityId, communityName, pubKey) {
chatColumnLayout.currentNotificationChatId = null
chatColumnLayout.currentNotificationCommunityId = communityId
systemTray.showMessage(qsTr("New membership request"),
qsTr("%1 asks to join %2").arg(Utils.getDisplayName(pubKey)).arg(communityName),
SystemTrayIcon.NoIcon,
Constants.notificationPopupTTL)
property var loadMsgs : Backpressure.oneInTime(chatLogView, 500, function() {
if(loadingMessages) return;
loadingMessages = true;
chatsModel.messageView.loadMoreMessages();
});
onContentYChanged: {
scrollDownButton.visible = (contentHeight - (scrollY + height) > 400)
if(scrollY < 500){
loadMsgs();
}
}
model: messageListDelegate
section.property: "sectionIdentifier"
section.criteria: ViewSection.FullString
}
property var loadMsgs : Backpressure.oneInTime(chatLogView, 500, function() {
if(loadingMessages) return;
loadingMessages = true;
chatsModel.messageView.loadMoreMessages();
});
onContentYChanged: {
scrollDownButton.visible = (contentHeight - (scrollY + height) > 400)
if(scrollY < 500){
loadMsgs();
}
MessageDialog {
id: sendingMsgFailedPopup
standardButtons: StandardButton.Ok
//% "Failed to send message."
text: qsTrId("failed-to-send-message-")
icon: StandardIcon.Critical
}
DelegateModelGeneralized {
id: messageListDelegate
lessThan: [
function(left, right) { return left.clock > right.clock }
]
model: messageListDelegate
section.property: "sectionIdentifier"
section.criteria: ViewSection.FullString
model: messageList
}
delegate: Message {
id: msgDelegate
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
replaces: model.replaces
isEdited: model.isEdited
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
authorCurrentMsg: msgDelegate.ListView.section
// The previous message is actually the nextSection since we reversed the list order
authorPrevMsg: msgDelegate.ListView.nextSection
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedMessage: model.isPinned
pinnedBy: model.pinnedBy
gapFrom: model.gapFrom
gapTo: model.gapTo
MessageDialog {
id: sendingMsgFailedPopup
standardButtons: StandardButton.Ok
//% "Failed to send message."
text: qsTrId("failed-to-send-message-")
icon: StandardIcon.Critical
}
DelegateModelGeneralized {
id: messageListDelegate
lessThan: [
function(left, right) { return left.clock > right.clock }
]
model: messageList
delegate: Message {
id: msgDelegate
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
alias: model.alias
localName: model.localName
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
outgoingStatus: model.outgoingStatus
replaces: model.replaces
isEdited: model.isEdited
responseTo: model.responseTo
authorCurrentMsg: msgDelegate.ListView.section
// The previous message is actually the nextSection since we reversed the list order
authorPrevMsg: msgDelegate.ListView.nextSection
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
linkUrls: model.linkUrls
communityId: model.communityId
hasMention: model.hasMention
stickerPackId: model.stickerPackId
pinnedMessage: model.isPinned
pinnedBy: model.pinnedBy
gapFrom: model.gapFrom
gapTo: model.gapTo
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
prevMessageIndex: {
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
if (msgDelegate.DelegateModel.itemsIndex < messageListDelegate.items.count - 1) {
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex + 1).model.index
prevMessageIndex: {
// This is used in order to have access to the previous message and determine the timestamp
// we can't rely on the index because the sequence of messages is not ordered on the nim side
if (msgDelegate.DelegateModel.itemsIndex < messageListDelegate.items.count - 1) {
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex + 1).model.index
}
return -1;
}
return -1;
}
nextMessageIndex: {
if (msgDelegate.DelegateModel.itemsIndex <= 1) {
return -1
nextMessageIndex: {
if (msgDelegate.DelegateModel.itemsIndex <= 1) {
return -1
}
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index
}
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index
scrollToBottom: chatLogView.scrollToBottom
timeout: model.timeout
}
}
}
Rectangle {
id: userList
visible: showUsers && chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne
property int defaultWidth: 250
SplitView.preferredWidth: visible ? defaultWidth : 0
SplitView.minimumWidth: 50
color: Style.current.secondaryMenuBackground
anchors.top: parent.top
anchors.bottom: parent.bottom
height: childrenRect.height
ListView {
id: userListView
anchors.fill: parent
anchors.bottomMargin: Style.current.bigPadding
spacing: 0
boundsBehavior: Flickable.StopAtBounds
model: userListDelegate
}
DelegateModelGeneralized {
id: userListDelegate
lessThan: [
function(left, right) {
return left.lastSeen > right.lastSeen
}
]
model: messageList.userList
delegate: User {
publicKey: model.publicKey
name: model.userName
identicon: model.identicon
lastSeen: model.lastSeen
currentTime: svRoot.currentTime
}
scrollToBottom: chatLogView.scrollToBottom
timeout: model.timeout
}
}
}

View File

@ -70,6 +70,19 @@ Item {
anchors.rightMargin: Style.current.smallPadding
spacing: 12
StatusIconButton {
id: showUsersBtn
anchors.verticalCenter: parent.verticalCenter
icon.name: "channel-icon-group"
iconColor: showUsers ? Style.current.contextMenuButtonForegroundColor : Style.current.contextMenuButtonBackgroundHoverColor
hoveredIconColor: Style.current.contextMenuButtonForegroundColor
highlightedBackgroundColor: Style.current.contextMenuButtonBackgroundHoverColor
onClicked: {
showUsers = !showUsers
}
visible: appSettings.showOnlineUsers && chatsModel.channelView.activeChannel.chatType !== Constants.chatTypeOneToOne
}
StatusContextMenuButton {
id: moreActionsBtn
anchors.verticalCenter: parent.verticalCenter

View File

@ -0,0 +1,125 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../../../../shared"
import "../../../../shared/status"
import "../../../../imports"
import "../components"
Item {
property string publicKey: ""
property string name: "channelName"
property string lastSeen: ""
property string identicon
property bool hovered: false
property bool enableMouseArea: true
property var currentTime
property color color: {
if (wrapper.hovered) {
return Style.current.menuBackgroundHover
}
return Style.current.transparent
}
property string profileImage: appMain.getProfileImage(publicKey) || ""
property bool isCurrentUser: publicKey === profileModel.profile.pubKey
id: wrapper
anchors.right: parent.right
anchors.top: applicationWindow.top
anchors.left: parent.left
height: rectangle.height + 4
Rectangle {
Connections {
target: profileModel.contacts.list
onContactChanged: {
if (pubkey === wrapper.publicKey) {
wrapper.profileImage = appMain.getProfileImage(wrapper.publicKey)
}
}
}
id: rectangle
color: wrapper.color
radius: 8
height: 40
width: parent.width
StatusIdenticon {
id: contactImage
height: 28
width: 28
chatId: wrapper.publicKey
chatName: wrapper.name
chatType: Constants.chatTypeOneToOne
identicon: wrapper.profileImage || wrapper.identicon
anchors.left: parent.left
anchors.leftMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: contactInfo
text: Emoji.parse(Utils.removeStatusEns(Utils.filterXSS(wrapper.name))) + (isCurrentUser ? " " + qsTrId("(you)") : "")
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
elide: Text.ElideRight
color: Style.current.textColor
font.weight: Font.Medium
font.pixelSize: 15
anchors.left: contactImage.right
anchors.leftMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
anchors.left: contactImage.right
anchors.leftMargin: -10
anchors.bottom: contactImage.bottom
height: 10
width: 10
radius: 20
color: {
let lastSeenMinutesAgo = (currentTime - parseInt(lastSeen)) / 1000 / 60
if (!chatsModel.isOnline) {
return Style.current.darkGrey
}
if (isCurrentUser || lastSeenMinutesAgo < 5){
return Style.current.green;
} else if (lastSeenMinutesAgo < 20) {
return Style.current.orange
}
return Style.current.darkGrey
}
}
MouseArea {
enabled: enableMouseArea
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
hoverEnabled: true
onEntered: {
wrapper.hovered = true
}
onExited: {
wrapper.hovered = false
}
onClicked: {
console.log("TODO: do something")
}
}
}
}
/*##^##
Designer {
D{i:0;formeditorColor:"#ffffff";height:64;width:640}
}
##^##*/

View File

@ -123,6 +123,15 @@ Item {
}
}
StatusSettingsLineButton {
text: qsTr("Online users")
isSwitch: true
switchChecked: appSettings.showOnlineUsers
onClicked: {
appSettings.showOnlineUsers = !appSettings.showOnlineUsers
}
}
// StatusSettingsLineButton {
// //% "Node Management"
// text: qsTrId("node-management")

View File

@ -326,6 +326,7 @@ StatusAppLayout {
property bool nodeManagementEnabled: false
property bool isBrowserEnabled: false
property bool isActivityCenterEnabled: false
property bool showOnlineUsers: false
property bool displayChatImages: false
property bool useCompactMode: true
property bool timelineEnabled: true

View File

@ -31,6 +31,7 @@ Theme {
property color tenPercentWhite: Qt.rgba(255, 255, 255, 0.1)
property color fivePercentBlack: "#E5E5E5"
property color tenPercentBlue: Qt.rgba(67, 96, 223, 0.1)
property color orange: "#FFA500"
property color background: "#2C2C2C"
property color appBarDividerColor: darkGrey

View File

@ -31,6 +31,7 @@ Theme {
property color tenPercentBlack: Qt.rgba(0, 0, 0, 0.1)
property color fivePercentBlack: "#E5E5E5"
property color tenPercentBlue: Qt.rgba(67, 96, 223, 0.1)
property color orange: "#FFA500"
property color background: white
property color appBarDividerColor: darkGrey