feat: introduce timeline

Closes #1489 #1490 #1491
This commit is contained in:
Pascal Precht 2020-12-17 11:40:37 +01:00 committed by Iuri Matias
parent 1ead1c3db5
commit 0767ce2443
19 changed files with 335 additions and 21 deletions

View File

@ -1,4 +1,5 @@
import sugar, sequtils, times, strutils import sugar, sequtils, times, strutils
import ../../status/chat/chat as status_chat
proc handleChatEvents(self: ChatController) = proc handleChatEvents(self: ChatController) =
# Display already saved messages # Display already saved messages
@ -39,6 +40,9 @@ proc handleChatEvents(self: ChatController) =
self.status.events.on("channelLoaded") do(e: Args): self.status.events.on("channelLoaded") do(e: Args):
var channel = ChannelArgs(e) var channel = ChannelArgs(e)
if channel.chat.chatType == ChatType.Timeline:
self.view.setTimelineChat(channel.chat)
elif channel.chat.chatType != ChatType.Profile:
discard self.view.chats.addChatItemToList(channel.chat) discard self.view.chats.addChatItemToList(channel.chat)
self.status.chat.chatMessages(channel.chat.id) self.status.chat.chatMessages(channel.chat.id)
self.status.chat.chatReactions(channel.chat.id) self.status.chat.chatReactions(channel.chat.id)
@ -50,13 +54,18 @@ proc handleChatEvents(self: ChatController) =
self.status.events.on("channelJoined") do(e: Args): self.status.events.on("channelJoined") do(e: Args):
var channel = ChannelArgs(e) var channel = ChannelArgs(e)
if channel.chat.chatType == ChatType.Timeline:
self.view.setTimelineChat(channel.chat)
elif channel.chat.chatType != ChatType.Profile:
discard self.view.chats.addChatItemToList(channel.chat) discard self.view.chats.addChatItemToList(channel.chat)
self.view.setActiveChannel(channel.chat.id)
self.status.chat.chatMessages(channel.chat.id) self.status.chat.chatMessages(channel.chat.id)
self.status.chat.chatReactions(channel.chat.id) self.status.chat.chatReactions(channel.chat.id)
self.view.setActiveChannel(channel.chat.id)
self.status.events.on("channelLeft") do(e: Args): self.status.events.on("channelLeft") do(e: Args):
self.view.removeChat(ChatIdArg(e).chatId) let chatId = ChatIdArg(e).chatId
self.view.removeChat(chatId)
self.view.removeMessagesFromTimeline(chatId)
self.status.events.on("activeChannelChanged") do(e: Args): self.status.events.on("activeChannelChanged") do(e: Args):
self.view.setActiveChannel(ChatIdArg(e).chatId) self.view.setActiveChannel(ChatIdArg(e).chatId)

View File

@ -4,6 +4,7 @@ import ../../status/mailservers
import ../../status/libstatus/accounts/constants import ../../status/libstatus/accounts/constants
import ../../status/libstatus/mailservers as status_mailservers import ../../status/libstatus/mailservers as status_mailservers
import ../../status/libstatus/types import ../../status/libstatus/types
import ../../status/libstatus/utils as status_utils
import ../../status/accounts as status_accounts import ../../status/accounts as status_accounts
import ../../status/chat as status_chat import ../../status/chat as status_chat
import ../../status/messages as status_messages import ../../status/messages as status_messages
@ -33,12 +34,14 @@ QtObject:
groups*: GroupsView groups*: GroupsView
transactions*: TransactionsView transactions*: TransactionsView
activeChannel*: ChatItemView activeChannel*: ChatItemView
previousActiveChannelIndex: int
replyTo: string replyTo: string
channelOpenTime*: Table[string, int64] channelOpenTime*: Table[string, int64]
connected: bool connected: bool
unreadMessageCnt: int unreadMessageCnt: int
oldestMessageTimestamp: int64 oldestMessageTimestamp: int64
loadingMessages: bool loadingMessages: bool
timelineChat: Chat
pubKey*: string pubKey*: string
proc setup(self: ChatsView) = self.QAbstractListModel.setup proc setup(self: ChatsView) = self.QAbstractListModel.setup
@ -71,6 +74,9 @@ QtObject:
result.transactions = newTransactionsView(status) result.transactions = newTransactionsView(status)
result.unreadMessageCnt = 0 result.unreadMessageCnt = 0
result.loadingMessages = false result.loadingMessages = false
result.previousActiveChannelIndex = -1
result.messageList[status_utils.getTimelineChatId()] = newChatMessageList(status_utils.getTimelineChatId(), result.status, false)
result.setup() result.setup()
proc oldestMessageTimestampChanged*(self: ChatsView) {.signal.} proc oldestMessageTimestampChanged*(self: ChatsView) {.signal.}
@ -205,6 +211,7 @@ QtObject:
if selectedChannel.chatType.isOneToOne and selectedChannel.id == selectedChannel.name: if selectedChannel.chatType.isOneToOne and selectedChannel.id == selectedChannel.name:
selectedChannel.name = self.userNameOrAlias(selectedChannel.id) selectedChannel.name = self.userNameOrAlias(selectedChannel.id)
self.previousActiveChannelIndex = index
self.activeChannel.setChatItem(selectedChannel) self.activeChannel.setChatItem(selectedChannel)
self.status.chat.setActiveChannel(selectedChannel.id) self.status.chat.setActiveChannel(selectedChannel.id)
@ -231,6 +238,16 @@ QtObject:
write = setActiveChannel write = setActiveChannel
notify = activeChannelChanged notify = activeChannelChanged
proc setActiveChannelToTimeline*(self: ChatsView) {.slot.} =
if not self.activeChannel.chatItem.isNil:
self.previousActiveChannelIndex = self.chats.chats.findIndexById(self.activeChannel.id)
self.activeChannel.setChatItem(self.timelineChat)
self.activeChannelChanged()
proc restorePreviousActiveChannel*(self: ChatsView) {.slot.} =
if self.activeChannel.id == self.timelineChat.id and not self.previousActiveChannelIndex == -1:
self.setActiveChannelByIndex(self.previousActiveChannelIndex)
proc getCurrentSuggestions(self: ChatsView): QVariant {.slot.} = proc getCurrentSuggestions(self: ChatsView): QVariant {.slot.} =
return newQVariant(self.currentSuggestions) return newQVariant(self.currentSuggestions)
@ -238,8 +255,11 @@ QtObject:
read = getCurrentSuggestions read = getCurrentSuggestions
proc upsertChannel(self: ChatsView, channel: string) = proc upsertChannel(self: ChatsView, channel: string) =
var chat: Chat = nil
if self.status.chat.channels.hasKey(channel):
chat = self.status.chat.channels[channel]
if not self.messageList.hasKey(channel): if not self.messageList.hasKey(channel):
self.messageList[channel] = newChatMessageList(channel, self.status) self.messageList[channel] = newChatMessageList(channel, self.status, not chat.isNil and chat.chatType != ChatType.Profile)
self.channelOpenTime[channel] = now().toTime.toUnix * 1000 self.channelOpenTime[channel] = now().toTime.toUnix * 1000
proc messagePushed*(self: ChatsView) {.signal.} proc messagePushed*(self: ChatsView) {.signal.}
@ -257,10 +277,15 @@ QtObject:
for msg in messages.mitems: for msg in messages.mitems:
self.upsertChannel(msg.chatId) self.upsertChannel(msg.chatId)
msg.userName = self.status.chat.getUserName(msg.fromAuthor, msg.alias) msg.userName = self.status.chat.getUserName(msg.fromAuthor, msg.alias)
if self.status.chat.channels.hasKey(msg.chatId):
let chat = self.status.chat.channels[msg.chatId]
if (chat.chatType == ChatType.Profile):
self.messageList[status_utils.getTimelineChatId()].add(msg)
else:
self.messageList[msg.chatId].add(msg) self.messageList[msg.chatId].add(msg)
self.messagePushed() self.messagePushed()
if self.channelOpenTime.getOrDefault(msg.chatId, high(int64)) < msg.timestamp.parseFloat.fromUnixFloat.toUnix: if self.channelOpenTime.getOrDefault(msg.chatId, high(int64)) < msg.timestamp.parseFloat.fromUnixFloat.toUnix:
let channel = self.chats.getChannelById(msg.chatId) let channel = self.status.chat.channels[msg.chatId]
let isAddedContact = channel.chatType.isOneToOne and self.status.contacts.isAdded(channel.id) let isAddedContact = channel.chatType.isOneToOne and self.status.contacts.isAdded(channel.id)
if not channel.muted: if not channel.muted:
self.messageNotificationPushed( self.messageNotificationPushed(
@ -274,12 +299,10 @@ QtObject:
msg.hasMention, msg.hasMention,
isAddedContact, isAddedContact,
channel.name) channel.name)
else: else:
discard self.status.chat.markMessagesSeen(msg.chatId, @[msg.id]) discard self.status.chat.markMessagesSeen(msg.chatId, @[msg.id])
self.newMessagePushed() self.newMessagePushed()
proc updateUsernames*(self:ChatsView, contacts: seq[Profile]) = proc updateUsernames*(self:ChatsView, contacts: seq[Profile]) =
if contacts.len > 0: if contacts.len > 0:
# Updating usernames for all the messages list # Updating usernames for all the messages list
@ -320,6 +343,9 @@ QtObject:
discard self.chats.addChatItemToList(chatItem) discard self.chats.addChatItemToList(chatItem)
self.messagePushed() self.messagePushed()
proc setTimelineChat*(self: ChatsView, chatItem: Chat) =
self.timelineChat = chatItem
proc copyToClipboard*(self: ChatsView, content: string) {.slot.} = proc copyToClipboard*(self: ChatsView, content: string) {.slot.} =
setClipBoardText(content) setClipBoardText(content)
@ -400,6 +426,10 @@ QtObject:
self.messageList[chatId].delete self.messageList[chatId].delete
self.messageList.del(chatId) self.messageList.del(chatId)
proc removeMessagesFromTimeline*(self: ChatsView, chatId: string) =
self.messageList[status_utils.getTimelineChatId()].deleteMessagesByChatId(chatId)
self.activeChannelChanged()
proc clearChatHistory*(self: ChatsView, id: string) {.slot.} = proc clearChatHistory*(self: ChatsView, id: string) {.slot.} =
self.status.chat.clearHistory(id) self.status.chat.clearHistory(id)

View File

@ -121,7 +121,7 @@ QtObject:
proc upsertChannel(self: ChannelsList, channel: Chat): int = proc upsertChannel(self: ChannelsList, channel: Chat): int =
let idx = self.chats.findIndexById(channel.id) let idx = self.chats.findIndexById(channel.id)
if idx == -1: if idx == -1:
if channel.isActive: if channel.isActive and channel.chatType != ChatType.Profile and channel.chatType != ChatType.Timeline:
# We only want to add a channel to the list if it is active # We only want to add a channel to the list if it is active
# otherwise, we'll end up with zombie channels on the list # otherwise, we'll end up with zombie channels on the list
result = self.addChatItemToList(channel) result = self.addChatItemToList(channel)

View File

@ -1,4 +1,4 @@
import NimQml, Tables, sets, json import NimQml, Tables, sets, json, sugar
import ../../../status/status import ../../../status/status
import ../../../status/accounts import ../../../status/accounts
import ../../../status/chat import ../../../status/chat
@ -64,9 +64,12 @@ QtObject:
result.contentType = ContentType.ChatIdentifier; result.contentType = ContentType.ChatIdentifier;
result.chatId = chatId result.chatId = chatId
proc newChatMessageList*(chatId: string, status: Status): ChatMessageList = proc newChatMessageList*(chatId: string, status: Status, addFakeMessages: bool = true): ChatMessageList =
new(result, delete) new(result, delete)
result.messages = @[result.chatIdentifier(chatId), result.fetchMoreMessagesButton()] result.messages = @[]
if addFakeMessages:
result.messages.add(result.chatIdentifier(chatId))
result.messages.add(result.fetchMoreMessagesButton())
result.messageIndex = initTable[string, int]() result.messageIndex = initTable[string, int]()
result.timedoutMessages = initHashSet[string]() result.timedoutMessages = initHashSet[string]()
result.status = status result.status = status
@ -81,6 +84,11 @@ QtObject:
self.messageReactions.del(messageId) self.messageReactions.del(messageId)
self.endRemoveRows() self.endRemoveRows()
proc deleteMessagesByChatId*(self: ChatMessageList, chatId: string) =
let messages = self.messages.filter(m => m.chatId == chatId)
for message in messages:
self.deleteMessage(message.id)
proc resetTimeOut*(self: ChatMessageList, messageId: string) = proc resetTimeOut*(self: ChatMessageList, messageId: string) =
if not self.messageIndex.hasKey(messageId): return if not self.messageIndex.hasKey(messageId): return
let msgIdx = self.messageIndex[messageId] let msgIdx = self.messageIndex[messageId]

View File

@ -1,6 +1,8 @@
import NimQml, chronicles, sequtils, sugar, strutils import NimQml, chronicles, sequtils, sugar, strutils
import ../../../status/libstatus/utils as status_utils
import ../../../status/status import ../../../status/status
import ../../../status/threads import ../../../status/threads
import ../../../status/chat/chat
import contact_list import contact_list
import ../../../status/profile/profile import ../../../status/profile/profile
import ../../../status/ens as status_ens import ../../../status/ens as status_ens
@ -135,6 +137,7 @@ QtObject:
proc addContact*(self: ContactsView, publicKey: string): string {.slot.} = proc addContact*(self: ContactsView, publicKey: string): string {.slot.} =
result = self.status.contacts.addContact(publicKey) result = self.status.contacts.addContact(publicKey)
self.status.chat.join(status_utils.getTimelineChatId(publicKey), ChatType.Profile, "", publicKey)
self.contactChanged(publicKey, true) self.contactChanged(publicKey, true)
proc changeContactNickname*(self: ContactsView, publicKey: string, nickname: string) {.slot.} = proc changeContactNickname*(self: ContactsView, publicKey: string, nickname: string) {.slot.} =
@ -153,4 +156,7 @@ QtObject:
proc removeContact*(self: ContactsView, publicKey: string) {.slot.} = proc removeContact*(self: ContactsView, publicKey: string) {.slot.} =
self.status.contacts.removeContact(publicKey) self.status.contacts.removeContact(publicKey)
let channelId = status_utils.getTimelineChatId(publicKey)
if self.status.chat.hasChannel(channelId):
self.status.chat.leave(channelId)
self.contactChanged(publicKey, false) self.contactChanged(publicKey, false)

View File

@ -1,9 +1,11 @@
import json, strutils, sequtils, tables, chronicles, times import json, strutils, sequtils, tables, chronicles, times, sugar
import libstatus/chat as status_chat import libstatus/chat as status_chat
import libstatus/mailservers as status_mailservers import libstatus/mailservers as status_mailservers
import libstatus/chatCommands as status_chat_commands import libstatus/chatCommands as status_chat_commands
import libstatus/accounts/constants as constants import libstatus/accounts/constants as constants
import libstatus/types import libstatus/types
import libstatus/utils as status_utils
import libstatus/contacts as status_contacts
import stickers import stickers
import ../eventemitter import ../eventemitter
@ -89,12 +91,12 @@ proc hasChannel*(self: ChatModel, chatId: string): bool =
proc getActiveChannel*(self: ChatModel): string = proc getActiveChannel*(self: ChatModel): string =
if (self.channels.len == 0): "" else: toSeq(self.channels.values)[self.channels.len - 1].id if (self.channels.len == 0): "" else: toSeq(self.channels.values)[self.channels.len - 1].id
proc join*(self: ChatModel, chatId: string, chatType: ChatType, ensName: string = "") = proc join*(self: ChatModel, chatId: string, chatType: ChatType, ensName: string = "", pubKey: string = "") =
if self.hasChannel(chatId): return if self.hasChannel(chatId): return
var chat = newChat(chatId, ChatType(chatType)) var chat = newChat(chatId, ChatType(chatType))
self.channels[chat.id] = chat self.channels[chat.id] = chat
status_chat.saveChat(chatId, chatType, true, chat.color, ensName) status_chat.saveChat(chatId, chatType, color=chat.color, ensName=ensName, profile=pubKey)
if ensName != "": if ensName != "":
chat.name = ensName chat.name = ensName
chat.ensName = ensName chat.ensName = ensName
@ -127,6 +129,32 @@ proc updateContacts*(self: ChatModel, contacts: seq[Profile]) =
proc init*(self: ChatModel, pubKey: string) = proc init*(self: ChatModel, pubKey: string) =
var chatList = status_chat.loadChats() var chatList = status_chat.loadChats()
var contacts = getAddedContacts()
let profileUpdatesChatIds = chatList.filter(c => c.chatType == ChatType.Profile).map(c => c.id)
if chatList.filter(c => c.chatType == ChatType.Timeline).len == 0:
var timelineChannel = newChat(status_utils.getTimelineChatId(), ChatType.Timeline)
self.join(timelineChannel.id, timelineChannel.chatType)
chatList.add(timelineChannel)
let timelineChatId = status_utils.getTimelineChatId(pubKey)
if not profileUpdatesChatIds.contains(timelineChatId):
var profileUpdateChannel = newChat(timelineChatId, ChatType.Profile)
status_chat.saveChat(profileUpdateChannel.id, profileUpdateChannel.chatType, profile=pubKey)
chatList.add(profileUpdateChannel)
# For profile updates and timeline, we have to make sure that for
# each added contact, a chat has been saved for the currently logged-in
# user. Users that will use a version of Status with timeline support for the
# first time, won't have any of those otherwise.
if profileUpdatesChatIds.filter(id => id != timelineChatId).len != contacts.len:
for contact in contacts:
if not profileUpdatesChatIds.contains(status_utils.getTimelineChatId(contact.address)):
let profileUpdatesChannel = newChat(status_utils.getTimelineChatId(contact.address), ChatType.Profile)
status_chat.saveChat(profileUpdatesChannel.id, profileUpdatesChannel.chatType, ensName=contact.ensName, profile=contact.address)
chatList.add(profileUpdatesChannel)
var filters:seq[JsonNode] = @[] var filters:seq[JsonNode] = @[]
for chat in chatList: for chat in chatList:

View File

@ -10,6 +10,7 @@ type ChatType* {.pure.}= enum
Timeline = 5 Timeline = 5
proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne
proc isTimeline*(self: ChatType): bool = self == ChatType.Timeline
type ChatMember* = object type ChatMember* = object
admin*: bool admin*: bool

View File

@ -39,7 +39,7 @@ proc removeChatFilters(self: ChatModel, chatId: string) =
for filter in filters: for filter in filters:
if filter["chatId"].getStr == chatId: if filter["chatId"].getStr == chatId:
status_chat.removeFilters(chatId, filter["filterId"].getStr) status_chat.removeFilters(chatId, filter["filterId"].getStr)
of ChatType.OneToOne: of ChatType.OneToOne, ChatType.Profile:
# Check if user does not belong to any active chat group # Check if user does not belong to any active chat group
var inGroup = false var inGroup = false
for channel in self.channels.values: for channel in self.channels.values:

View File

@ -1,6 +1,10 @@
import json, sequtils, sugar import json, sequtils, sugar
import libstatus/contacts as status_contacts import libstatus/contacts as status_contacts
import libstatus/accounts as status_accounts import libstatus/accounts as status_accounts
import libstatus/chat as status_chat
import libstatus/utils as status_utils
import chat/chat
#import chat/utils
import profile/profile import profile/profile
import ../eventemitter import ../eventemitter
@ -69,6 +73,7 @@ proc addContact*(self: ContactModel, id: string, localNickname: string): string
let updating = contact.systemTags.contains(":contact/added") let updating = contact.systemTags.contains(":contact/added")
if not updating: if not updating:
contact.systemTags.add(":contact/added") contact.systemTags.add(":contact/added")
status_chat.saveChat(getTimelineChatId(contact.id), ChatType.Profile, ensName=contact.ensName, profile=contact.id)
let nickname = let nickname =
if (localNickname == ""): if (localNickname == ""):
contact.localNickname contact.localNickname

View File

@ -18,7 +18,7 @@ proc removeFilters*(chatId: string, filterId: string) =
[{ "ChatID": chatId, "FilterID": filterId }] [{ "ChatID": chatId, "FilterID": filterId }]
]) ])
proc saveChat*(chatId: string, chatType: ChatType, active: bool = true, color: string, ensName: string = "", profile: string = "") = proc saveChat*(chatId: string, chatType: ChatType, active: bool = true, color: string = "#000000", ensName: string = "", profile: string = "") =
# TODO: ideally status-go/stimbus should handle some of these fields instead of having the client # TODO: ideally status-go/stimbus should handle some of these fields instead of having the client
# send them: lastMessage, unviewedMEssagesCount, timestamp, lastClockValue, name? # send them: lastMessage, unviewedMEssagesCount, timestamp, lastClockValue, name?
discard callPrivateRPC("saveChat".prefix, %* [ discard callPrivateRPC("saveChat".prefix, %* [

View File

@ -4,6 +4,12 @@ from times import getTime, toUnix, nanosecond
import accounts/signing_phrases import accounts/signing_phrases
from web3 import Address, fromHex from web3 import Address, fromHex
proc getTimelineChatId*(pubKey: string = ""): string =
if pubKey == "":
return "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a"
else:
return "@" & pubKey
proc isWakuEnabled(): bool = proc isWakuEnabled(): bool =
true # TODO: true # TODO:

View File

@ -98,7 +98,8 @@ proc newChat*(id: string, chatType: ChatType): Chat =
lastClockValue: 0, lastClockValue: 0,
deletedAtClockValue: 0, deletedAtClockValue: 0,
unviewedMessagesCount: 0, unviewedMessagesCount: 0,
hasMentions: false hasMentions: false,
members: @[]
) )
if chatType == ChatType.OneToOne: if chatType == ChatType.OneToOne:

View File

@ -266,7 +266,7 @@ StackLayout {
} }
onSendMessage: { onSendMessage: {
if (chatInput.fileUrls.length > 0){ if (chatInput.fileUrls.length > 0){
chatsModel.sendImage(chatInput.fileUrls[0]); chatsModel.sendImage(chatInput.fileUrls[0], false);
} }
var msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text)) var msg = chatsModel.plainText(Emoji.deparse(chatInput.textInput.text))
if (msg.length > 0){ if (msg.length > 0){

View File

@ -13,6 +13,7 @@ SplitView {
property alias chatColumn: chatColumn property alias chatColumn: chatColumn
property var onActivated: function () { property var onActivated: function () {
chatsModel.restorePreviousActiveChannel()
chatColumn.onActivated() chatColumn.onActivated()
} }

View File

@ -0,0 +1,197 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import QtQml.Models 2.13
import QtQuick.Layouts 1.13
import "../../../imports"
import "../../../shared"
import "../../../shared/status"
import "../Chat/data"
import "../Chat/ChatColumn"
import "../Chat/components"
ScrollView {
id: root
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: timelineContainer.height + 40
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
property var onActivated: function () {
chatsModel.setActiveChannelToTimeline()
statusUpdateInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
Component.onCompleted: {
statusUpdateInput.textInput.forceActiveFocus(Qt.MouseFocusReason)
}
function openProfilePopup(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, parentPopup){
var popup = profilePopupComponent.createObject(root);
if(parentPopup){
popup.parentPopup = parentPopup;
}
popup.openPopup(profileModel.profile.pubKey !== fromAuthorParam, userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam);
}
MessageContextMenu {
id: messageContextMenu
}
StatusImageModal {
id: imagePopup
}
EmojiReactions {
id: reactionModel
}
property Component profilePopupComponent: ProfilePopup {
id: profilePopup
height: 450
onClosed: {
if(profilePopup.parentPopup){
profilePopup.parentPopup.close();
}
destroy()
}
}
Rectangle {
id: timelineContainer
width: 624
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
height: childrenRect.height
color: "transparent"
StatusChatInput {
id: statusUpdateInput
anchors.top: parent.top
anchors.topMargin: 40
chatType: Constants.chatTypeStatusUpdate
onSendMessage: {
if (statusUpdateInput.fileUrls.length > 0){
chatsModel.sendImage(statusUpdateInput.fileUrls[0], true);
}
var msg = chatsModel.plainText(Emoji.deparse(statusUpdateInput.textInput.text))
if (msg.length > 0){
msg = statusUpdateInput.interpretMessage(msg)
chatsModel.sendMessage(msg, "", Utils.isOnlyEmoji(msg) ? Constants.emojiType : Constants.messageType, true);
statusUpdateInput.textInput.text = "";
if(event) event.accepted = true
statusUpdateInput.messageSound.stop()
Qt.callLater(statusUpdateInput.messageSound.play);
}
}
}
EmptyTimeline {
id: emptyTimeline
anchors.top: statusUpdateInput.bottom
anchors.topMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
visible: chatsModel.messageList.rowCount() === 0
}
ListView {
id: chatLogView
anchors.top: statusUpdateInput.bottom
anchors.topMargin: 40
anchors.left: parent.left
anchors.right: parent.right
height: childrenRect.height + 40
spacing: 10
flickDeceleration: 10000
interactive: false
model: messageListDelegate
section.property: "sectionIdentifier"
section.criteria: ViewSection.FullString
}
DelegateModel {
id: messageListDelegate
property var moreThan: [
function(left, right) { return left.clock > right.clock }
]
property int sortOrder: 0
onSortOrderChanged: items.setGroups(0, items.count, "unsorted")
function insertPosition(moreThan, item) {
var lower = 0
var upper = items.count
while (lower < upper) {
var middle = Math.floor(lower + (upper - lower) / 2)
var result = moreThan(item.model, items.get(middle).model);
if (result) {
upper = middle
} else {
lower = middle + 1
}
}
return lower
}
function sort(moreThan) {
while (unsortedItems.count > 0) {
var item = unsortedItems.get(0)
var index = insertPosition(moreThan, item)
item.groups = "items"
items.move(item.itemsIndex, index)
}
}
items.includeByDefault: false
groups: DelegateModelGroup {
id: unsortedItems
name: "unsorted"
includeByDefault: true
onChanged: {
if (messageListDelegate.sortOrder == messageListDelegate.moreThan.length)
setGroups(0, count, "items")
else {
messageListDelegate.sort(messageListDelegate.moreThan[messageListDelegate.sortOrder])
}
}
}
model: chatsModel.messageList
delegate: Message {
id: msgDelegate
fromAuthor: model.fromAuthor
chatId: model.chatId
userName: model.userName
localName: model.localName
alias: model.alias
message: model.message
plainText: model.plainText
identicon: model.identicon
isCurrentUser: model.isCurrentUser
timestamp: model.timestamp
sticker: model.sticker
contentType: model.contentType
outgoingStatus: model.outgoingStatus
responseTo: model.responseTo
authorCurrentMsg: msgDelegate.ListView.section
authorPrevMsg: msgDelegate.ListView.previousSection
imageClick: imagePopup.openPopup.bind(imagePopup)
messageId: model.messageId
emojiReactions: model.emojiReactions
isStatusUpdate: true
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 > 0){
return messageListDelegate.items.get(msgDelegate.DelegateModel.itemsIndex - 1).model.index
}
return -1;
}
timeout: model.timeout
}
}
}
}

View File

@ -5,6 +5,7 @@ import "../imports"
import "../shared" import "../shared"
import "../shared/status" import "../shared/status"
import "./AppLayouts" import "./AppLayouts"
import "./AppLayouts/Timeline"
import "./AppLayouts/Wallet" import "./AppLayouts/Wallet"
RowLayout { RowLayout {
@ -186,9 +187,16 @@ RowLayout {
icon.name: "compass" icon.name: "compass"
} }
StatusIconTabButton {
id: timelineBtn
anchors.top: browserBtn.enabled ? browserBtn.top : walletBtn.top
enabled: isExperimental === "1" || appSettings.timelineEnabled
icon.name: "timeline"
}
StatusIconTabButton { StatusIconTabButton {
id: profileBtn id: profileBtn
anchors.top: browserBtn.top anchors.top: timelineBtn.enabled ? timelineBtn.top : browserBtn.top
icon.name: "profile" icon.name: "profile"
Rectangle { Rectangle {
@ -284,6 +292,13 @@ RowLayout {
property var _web3Provider: web3Provider property var _web3Provider: web3Provider
} }
TimelineLayout {
id: timelineLayoutContainer
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.fillHeight: true
}
ProfileLayout { ProfileLayout {
id: profileLayoutContainer id: profileLayoutContainer
Layout.fillWidth: true Layout.fillWidth: true

4
ui/app/img/timeline.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM10 18.5C11.3807 18.5 12.5 17.3807 12.5 16C12.5 14.6193 11.3807 13.5 10 13.5C8.61929 13.5 7.5 14.6193 7.5 16C7.5 17.3807 8.61929 18.5 10 18.5ZM18.5 10C18.5 11.8106 17.9339 13.4889 16.9691 14.8677C16.7804 15.1373 16.3583 14.8961 16.4121 14.5715C16.4699 14.2229 16.5 13.865 16.5 13.5C16.5 9.91015 13.5899 7 10 7C6.41015 7 3.5 9.91015 3.5 13.5C3.5 13.865 3.53008 14.2229 3.5879 14.5715C3.64173 14.896 3.21955 15.1372 3.03094 14.8677C2.06609 13.4889 1.5 11.8106 1.5 10C1.5 5.30558 5.30558 1.5 10 1.5C14.6944 1.5 18.5 5.30558 18.5 10ZM14 16C14 16.1504 14.1963 16.2238 14.2745 16.0954C14.7349 15.3388 15 14.4504 15 13.5C15 10.7386 12.7614 8.5 10 8.5C7.23858 8.5 5 10.7386 5 13.5C5 14.4504 5.26515 15.3388 5.7255 16.0954C5.80367 16.2238 6 16.1504 6 16C6 13.7909 7.79086 12 10 12C12.2091 12 14 13.7909 14 16Z" fill="#939BA1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -96,6 +96,7 @@ ApplicationWindow {
property bool walletEnabled: false property bool walletEnabled: false
property bool browserEnabled: false property bool browserEnabled: false
property bool displayChatImages: false property bool displayChatImages: false
property bool timelineEnabled: true
property bool compactMode property bool compactMode
property string locale: "en" property string locale: "en"
property var recentEmojis: [] property var recentEmojis: []
@ -136,6 +137,7 @@ ApplicationWindow {
property bool browserEnabled: defaultAppSettings.browserEnabled property bool browserEnabled: defaultAppSettings.browserEnabled
property bool displayChatImages: defaultAppSettings.displayChatImages property bool displayChatImages: defaultAppSettings.displayChatImages
property bool compactMode: defaultAppSettings.compactMode property bool compactMode: defaultAppSettings.compactMode
property bool timelineEnabled: defaultAppSettings.timelineEnabled
property string locale: defaultAppSettings.locale property string locale: defaultAppSettings.locale
property var recentEmojis: defaultAppSettings.recentEmojis property var recentEmojis: defaultAppSettings.recentEmojis
property real volume: defaultAppSettings.volume property real volume: defaultAppSettings.volume

View File

@ -35,6 +35,7 @@ SOURCES = *.qml \
app/AppLayouts/Profile/Sections/*.qml \ app/AppLayouts/Profile/Sections/*.qml \
app/AppLayouts/Profile/Sections/Contacts/*.qml \ app/AppLayouts/Profile/Sections/Contacts/*.qml \
app/AppLayouts/Profile/Sections/Ens/*.qml \ app/AppLayouts/Profile/Sections/Ens/*.qml \
app/AppLayouts/Timeline/*.qml\
app/AppLayouts/Wallet/*.qml \ app/AppLayouts/Wallet/*.qml \
app/AppLayouts/Wallet/components/*.qml \ app/AppLayouts/Wallet/components/*.qml \
app/AppLayouts/Wallet/components/collectiblesComponents/*.qml \ app/AppLayouts/Wallet/components/collectiblesComponents/*.qml \