diff --git a/src/app/chat/event_handling.nim b/src/app/chat/event_handling.nim index 0ccb76f263..df5eb43f3b 100644 --- a/src/app/chat/event_handling.nim +++ b/src/app/chat/event_handling.nim @@ -1,4 +1,5 @@ import sugar, sequtils, times, strutils +import ../../status/chat/chat as status_chat proc handleChatEvents(self: ChatController) = # Display already saved messages @@ -39,7 +40,10 @@ proc handleChatEvents(self: ChatController) = self.status.events.on("channelLoaded") do(e: Args): var channel = ChannelArgs(e) - discard self.view.chats.addChatItemToList(channel.chat) + if channel.chat.chatType == ChatType.Timeline: + self.view.setTimelineChat(channel.chat) + elif channel.chat.chatType != ChatType.Profile: + discard self.view.chats.addChatItemToList(channel.chat) self.status.chat.chatMessages(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): var channel = ChannelArgs(e) - discard self.view.chats.addChatItemToList(channel.chat) + if channel.chat.chatType == ChatType.Timeline: + self.view.setTimelineChat(channel.chat) + elif channel.chat.chatType != ChatType.Profile: + discard self.view.chats.addChatItemToList(channel.chat) + self.view.setActiveChannel(channel.chat.id) self.status.chat.chatMessages(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.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.view.setActiveChannel(ChatIdArg(e).chatId) diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index b9fdf530bb..235f942a26 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -4,6 +4,7 @@ import ../../status/mailservers import ../../status/libstatus/accounts/constants import ../../status/libstatus/mailservers as status_mailservers import ../../status/libstatus/types +import ../../status/libstatus/utils as status_utils import ../../status/accounts as status_accounts import ../../status/chat as status_chat import ../../status/messages as status_messages @@ -33,12 +34,14 @@ QtObject: groups*: GroupsView transactions*: TransactionsView activeChannel*: ChatItemView + previousActiveChannelIndex: int replyTo: string channelOpenTime*: Table[string, int64] connected: bool unreadMessageCnt: int oldestMessageTimestamp: int64 loadingMessages: bool + timelineChat: Chat pubKey*: string proc setup(self: ChatsView) = self.QAbstractListModel.setup @@ -71,6 +74,9 @@ QtObject: result.transactions = newTransactionsView(status) result.unreadMessageCnt = 0 result.loadingMessages = false + result.previousActiveChannelIndex = -1 + result.messageList[status_utils.getTimelineChatId()] = newChatMessageList(status_utils.getTimelineChatId(), result.status, false) + result.setup() proc oldestMessageTimestampChanged*(self: ChatsView) {.signal.} @@ -205,6 +211,7 @@ QtObject: if selectedChannel.chatType.isOneToOne and selectedChannel.id == selectedChannel.name: selectedChannel.name = self.userNameOrAlias(selectedChannel.id) + self.previousActiveChannelIndex = index self.activeChannel.setChatItem(selectedChannel) self.status.chat.setActiveChannel(selectedChannel.id) @@ -231,6 +238,16 @@ QtObject: write = setActiveChannel 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.} = return newQVariant(self.currentSuggestions) @@ -238,8 +255,11 @@ QtObject: read = getCurrentSuggestions 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): - 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 proc messagePushed*(self: ChatsView) {.signal.} @@ -257,10 +277,15 @@ QtObject: for msg in messages.mitems: self.upsertChannel(msg.chatId) msg.userName = self.status.chat.getUserName(msg.fromAuthor, msg.alias) - self.messageList[msg.chatId].add(msg) + 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.messagePushed() 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) if not channel.muted: self.messageNotificationPushed( @@ -274,12 +299,10 @@ QtObject: msg.hasMention, isAddedContact, channel.name) - else: discard self.status.chat.markMessagesSeen(msg.chatId, @[msg.id]) self.newMessagePushed() - proc updateUsernames*(self:ChatsView, contacts: seq[Profile]) = if contacts.len > 0: # Updating usernames for all the messages list @@ -320,6 +343,9 @@ QtObject: discard self.chats.addChatItemToList(chatItem) self.messagePushed() + proc setTimelineChat*(self: ChatsView, chatItem: Chat) = + self.timelineChat = chatItem + proc copyToClipboard*(self: ChatsView, content: string) {.slot.} = setClipBoardText(content) @@ -400,6 +426,10 @@ QtObject: self.messageList[chatId].delete 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.} = self.status.chat.clearHistory(id) diff --git a/src/app/chat/views/channels_list.nim b/src/app/chat/views/channels_list.nim index 953792b709..7535da1d20 100644 --- a/src/app/chat/views/channels_list.nim +++ b/src/app/chat/views/channels_list.nim @@ -121,7 +121,7 @@ QtObject: proc upsertChannel(self: ChannelsList, channel: Chat): int = let idx = self.chats.findIndexById(channel.id) 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 # otherwise, we'll end up with zombie channels on the list result = self.addChatItemToList(channel) diff --git a/src/app/chat/views/message_list.nim b/src/app/chat/views/message_list.nim index 9a83727678..76d226ca5c 100644 --- a/src/app/chat/views/message_list.nim +++ b/src/app/chat/views/message_list.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, sets, json +import NimQml, Tables, sets, json, sugar import ../../../status/status import ../../../status/accounts import ../../../status/chat @@ -64,9 +64,12 @@ QtObject: result.contentType = ContentType.ChatIdentifier; result.chatId = chatId - proc newChatMessageList*(chatId: string, status: Status): ChatMessageList = + proc newChatMessageList*(chatId: string, status: Status, addFakeMessages: bool = true): ChatMessageList = 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.timedoutMessages = initHashSet[string]() result.status = status @@ -81,6 +84,11 @@ QtObject: self.messageReactions.del(messageId) 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) = if not self.messageIndex.hasKey(messageId): return let msgIdx = self.messageIndex[messageId] diff --git a/src/app/profile/views/contacts.nim b/src/app/profile/views/contacts.nim index e5d5e3881b..b22214c2ab 100644 --- a/src/app/profile/views/contacts.nim +++ b/src/app/profile/views/contacts.nim @@ -1,6 +1,8 @@ import NimQml, chronicles, sequtils, sugar, strutils +import ../../../status/libstatus/utils as status_utils import ../../../status/status import ../../../status/threads +import ../../../status/chat/chat import contact_list import ../../../status/profile/profile import ../../../status/ens as status_ens @@ -135,6 +137,7 @@ QtObject: proc addContact*(self: ContactsView, publicKey: string): string {.slot.} = result = self.status.contacts.addContact(publicKey) + self.status.chat.join(status_utils.getTimelineChatId(publicKey), ChatType.Profile, "", publicKey) self.contactChanged(publicKey, true) proc changeContactNickname*(self: ContactsView, publicKey: string, nickname: string) {.slot.} = @@ -153,4 +156,7 @@ QtObject: proc removeContact*(self: ContactsView, publicKey: string) {.slot.} = 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) diff --git a/src/status/chat.nim b/src/status/chat.nim index cccdf21764..6e4318b8b4 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -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/mailservers as status_mailservers import libstatus/chatCommands as status_chat_commands import libstatus/accounts/constants as constants import libstatus/types +import libstatus/utils as status_utils +import libstatus/contacts as status_contacts import stickers import ../eventemitter @@ -89,12 +91,12 @@ proc hasChannel*(self: ChatModel, chatId: string): bool = proc getActiveChannel*(self: ChatModel): string = 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 var chat = newChat(chatId, ChatType(chatType)) 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 != "": chat.name = ensName chat.ensName = ensName @@ -127,6 +129,32 @@ proc updateContacts*(self: ChatModel, contacts: seq[Profile]) = proc init*(self: ChatModel, pubKey: string) = 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] = @[] for chat in chatList: diff --git a/src/status/chat/chat.nim b/src/status/chat/chat.nim index 684e81b5ae..0f0d438e00 100644 --- a/src/status/chat/chat.nim +++ b/src/status/chat/chat.nim @@ -10,6 +10,7 @@ type ChatType* {.pure.}= enum Timeline = 5 proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne +proc isTimeline*(self: ChatType): bool = self == ChatType.Timeline type ChatMember* = object admin*: bool diff --git a/src/status/chat/utils.nim b/src/status/chat/utils.nim index 4371d18d7d..82b5c0b5e0 100644 --- a/src/status/chat/utils.nim +++ b/src/status/chat/utils.nim @@ -39,7 +39,7 @@ proc removeChatFilters(self: ChatModel, chatId: string) = for filter in filters: if filter["chatId"].getStr == chatId: 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 var inGroup = false for channel in self.channels.values: diff --git a/src/status/contacts.nim b/src/status/contacts.nim index 265aad2512..e0d6001451 100644 --- a/src/status/contacts.nim +++ b/src/status/contacts.nim @@ -1,6 +1,10 @@ import json, sequtils, sugar import libstatus/contacts as status_contacts 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 ../eventemitter @@ -69,6 +73,7 @@ proc addContact*(self: ContactModel, id: string, localNickname: string): string let updating = contact.systemTags.contains(":contact/added") if not updating: contact.systemTags.add(":contact/added") + status_chat.saveChat(getTimelineChatId(contact.id), ChatType.Profile, ensName=contact.ensName, profile=contact.id) let nickname = if (localNickname == ""): contact.localNickname diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index d53a9501e7..4aa6521473 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -18,7 +18,7 @@ proc removeFilters*(chatId: string, filterId: string) = [{ "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 # send them: lastMessage, unviewedMEssagesCount, timestamp, lastClockValue, name? discard callPrivateRPC("saveChat".prefix, %* [ diff --git a/src/status/libstatus/utils.nim b/src/status/libstatus/utils.nim index bfd6f42a9d..411c34c3b8 100644 --- a/src/status/libstatus/utils.nim +++ b/src/status/libstatus/utils.nim @@ -4,6 +4,12 @@ from times import getTime, toUnix, nanosecond import accounts/signing_phrases from web3 import Address, fromHex +proc getTimelineChatId*(pubKey: string = ""): string = + if pubKey == "": + return "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a" + else: + return "@" & pubKey + proc isWakuEnabled(): bool = true # TODO: diff --git a/src/status/signals/messages.nim b/src/status/signals/messages.nim index d20da6b01c..9b91c97459 100644 --- a/src/status/signals/messages.nim +++ b/src/status/signals/messages.nim @@ -98,7 +98,8 @@ proc newChat*(id: string, chatType: ChatType): Chat = lastClockValue: 0, deletedAtClockValue: 0, unviewedMessagesCount: 0, - hasMentions: false + hasMentions: false, + members: @[] ) if chatType == ChatType.OneToOne: diff --git a/ui/app/AppLayouts/Chat/ChatColumn.qml b/ui/app/AppLayouts/Chat/ChatColumn.qml index 1943dcf0a6..6c140bc3b0 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn.qml @@ -266,7 +266,7 @@ StackLayout { } onSendMessage: { 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)) if (msg.length > 0){ diff --git a/ui/app/AppLayouts/Chat/ChatLayout.qml b/ui/app/AppLayouts/Chat/ChatLayout.qml index 5416348176..81622c133f 100644 --- a/ui/app/AppLayouts/Chat/ChatLayout.qml +++ b/ui/app/AppLayouts/Chat/ChatLayout.qml @@ -13,6 +13,7 @@ SplitView { property alias chatColumn: chatColumn property var onActivated: function () { + chatsModel.restorePreviousActiveChannel() chatColumn.onActivated() } diff --git a/ui/app/AppLayouts/Timeline/TimelineLayout.qml b/ui/app/AppLayouts/Timeline/TimelineLayout.qml new file mode 100644 index 0000000000..00677c6329 --- /dev/null +++ b/ui/app/AppLayouts/Timeline/TimelineLayout.qml @@ -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 + } + } + } +} diff --git a/ui/app/AppMain.qml b/ui/app/AppMain.qml index a17bf7bbdd..e03580ae11 100644 --- a/ui/app/AppMain.qml +++ b/ui/app/AppMain.qml @@ -5,6 +5,7 @@ import "../imports" import "../shared" import "../shared/status" import "./AppLayouts" +import "./AppLayouts/Timeline" import "./AppLayouts/Wallet" RowLayout { @@ -186,9 +187,16 @@ RowLayout { icon.name: "compass" } + StatusIconTabButton { + id: timelineBtn + anchors.top: browserBtn.enabled ? browserBtn.top : walletBtn.top + enabled: isExperimental === "1" || appSettings.timelineEnabled + icon.name: "timeline" + } + StatusIconTabButton { id: profileBtn - anchors.top: browserBtn.top + anchors.top: timelineBtn.enabled ? timelineBtn.top : browserBtn.top icon.name: "profile" Rectangle { @@ -284,6 +292,13 @@ RowLayout { property var _web3Provider: web3Provider } + TimelineLayout { + id: timelineLayoutContainer + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillHeight: true + } + ProfileLayout { id: profileLayoutContainer Layout.fillWidth: true diff --git a/ui/app/img/timeline.svg b/ui/app/img/timeline.svg new file mode 100644 index 0000000000..f3abaccccb --- /dev/null +++ b/ui/app/img/timeline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/main.qml b/ui/main.qml index 4df7b9f35a..a20e7eb3bb 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -96,6 +96,7 @@ ApplicationWindow { property bool walletEnabled: false property bool browserEnabled: false property bool displayChatImages: false + property bool timelineEnabled: true property bool compactMode property string locale: "en" property var recentEmojis: [] @@ -136,6 +137,7 @@ ApplicationWindow { property bool browserEnabled: defaultAppSettings.browserEnabled property bool displayChatImages: defaultAppSettings.displayChatImages property bool compactMode: defaultAppSettings.compactMode + property bool timelineEnabled: defaultAppSettings.timelineEnabled property string locale: defaultAppSettings.locale property var recentEmojis: defaultAppSettings.recentEmojis property real volume: defaultAppSettings.volume diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index 645e00bbca..d86f73827b 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -35,6 +35,7 @@ SOURCES = *.qml \ app/AppLayouts/Profile/Sections/*.qml \ app/AppLayouts/Profile/Sections/Contacts/*.qml \ app/AppLayouts/Profile/Sections/Ens/*.qml \ + app/AppLayouts/Timeline/*.qml\ app/AppLayouts/Wallet/*.qml \ app/AppLayouts/Wallet/components/*.qml \ app/AppLayouts/Wallet/components/collectiblesComponents/*.qml \