From ce3252fb8fd56e4be06bfb176c2a58e188a112a0 Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Fri, 11 Dec 2020 15:29:46 -0500 Subject: [PATCH] wip community --- src/app/chat/event_handling.nim | 4 + src/app/chat/view.nim | 159 ++++++++- src/app/chat/views/channels_list.nim | 14 +- src/app/chat/views/community_item.nim | 85 +++++ src/app/chat/views/community_list.nim | 97 ++++++ src/status/chat.nim | 18 ++ src/status/chat/chat.nim | 32 ++ src/status/chat/message.nim | 1 + src/status/libstatus/chat.nim | 91 +++++- src/status/signals/messages.nim | 25 ++ ui/app/AppLayouts/Chat/ChatLayout.qml | 34 +- ui/app/AppLayouts/Chat/CommunityColumn.qml | 147 +++++++++ ui/app/AppLayouts/Chat/ContactsColumn.qml | 96 +++++- .../Chat/ContactsColumn/AddChat.qml | 15 +- .../Chat/ContactsColumn/Channel.qml | 2 +- .../Chat/ContactsColumn/ChannelList.qml | 123 +++---- .../Chat/ContactsColumn/CommunityButton.qml | 99 ++++++ .../CreateChannelPopup.qml | 149 +++++++++ .../Chat/ContactsColumn/CommunityList.qml | 78 +++++ .../ContactsColumn/CommunityWelcomeBanner.qml | 81 +++++ .../Chat/components/CommunitiesPopup.qml | 104 ++++++ .../Chat/components/CommunityDetailPopup.qml | 175 ++++++++++ .../Chat/components/CreateCommunityPopup.qml | 301 ++++++++++++++++++ ui/app/img/communities.svg | 3 + ui/app/img/member.svg | 4 + ui/imports/Constants.qml | 1 + ui/imports/Utils.qml | 4 + ui/nim-status-client.pro | 8 + ui/shared/StyledTextArea.qml | 6 +- ui/shared/status/StatusLetterIdenticon.qml | 2 +- vendor/status-go | 2 +- 31 files changed, 1851 insertions(+), 109 deletions(-) create mode 100644 src/app/chat/views/community_item.nim create mode 100644 src/app/chat/views/community_list.nim create mode 100644 ui/app/AppLayouts/Chat/CommunityColumn.qml create mode 100644 ui/app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml create mode 100644 ui/app/AppLayouts/Chat/ContactsColumn/CommunityComponents/CreateChannelPopup.qml create mode 100644 ui/app/AppLayouts/Chat/ContactsColumn/CommunityList.qml create mode 100644 ui/app/AppLayouts/Chat/ContactsColumn/CommunityWelcomeBanner.qml create mode 100644 ui/app/AppLayouts/Chat/components/CommunitiesPopup.qml create mode 100644 ui/app/AppLayouts/Chat/components/CommunityDetailPopup.qml create mode 100644 ui/app/AppLayouts/Chat/components/CreateCommunityPopup.qml create mode 100644 ui/app/img/communities.svg create mode 100644 ui/app/img/member.svg diff --git a/src/app/chat/event_handling.nim b/src/app/chat/event_handling.nim index df5eb43f3b..5eb3b3c2e4 100644 --- a/src/app/chat/event_handling.nim +++ b/src/app/chat/event_handling.nim @@ -52,6 +52,10 @@ proc handleChatEvents(self: ChatController) = self.view.setActiveChannelByIndex(0) self.view.appReady() + self.status.events.on("communityActiveChanged") do(e:Args): + # TODO set this back to the previous one instead + self.view.setActiveChannelByIndex(0) + self.status.events.on("channelJoined") do(e: Args): var channel = ChannelArgs(e) if channel.chat.chatType == ChatType.Timeline: diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index 688224241d..52254b5be3 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -14,7 +14,7 @@ import ../../status/chat/[chat, message] import ../../status/profile/profile import web3/[conversions, ethtypes] import ../../status/threads -import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions] +import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, community_list, community_item] import json_serialization import ../utils/image_utils @@ -35,6 +35,10 @@ QtObject: transactions*: TransactionsView activeChannel*: ChatItemView previousActiveChannelIndex: int + activeCommunity*: CommunityItemView + observedCommunity*: CommunityItemView + communityList*: CommunityList + joinedCommunityList*: CommunityList replyTo: string channelOpenTime*: Table[string, int64] connected: bool @@ -49,6 +53,8 @@ QtObject: proc delete(self: ChatsView) = self.chats.delete self.activeChannel.delete + self.observedCommunity.delete + self.activeCommunity.delete self.currentSuggestions.delete for msg in self.messageList.values: msg.delete @@ -66,11 +72,15 @@ QtObject: result.connected = false result.chats = newChannelsList(status) result.activeChannel = newChatItemView(status) + result.activeCommunity = newCommunityItemView(status) + result.observedCommunity = newCommunityItemView(status) result.currentSuggestions = newSuggestionsList() result.messageList = initTable[string, ChatMessageList]() result.reactions = newReactionView(status, result.messageList.addr, result.activeChannel) result.stickers = newStickersView(status, result.activeChannel) result.groups = newGroupsView(status,result.activeChannel) + result.communityList = newCommunityList(status) + result.joinedCommunityList = newCommunityList(status) result.transactions = newTransactionsView(status) result.unreadMessageCnt = 0 result.loadingMessages = false @@ -200,12 +210,19 @@ QtObject: discard self.status.chat.markAllChannelMessagesRead(selectedChannel.id) proc setActiveChannelByIndex*(self: ChatsView, index: int) {.slot.} = - if(self.chats.chats.len == 0): return + if((self.activeCommunity.active and self.activeCommunity.chats.chats.len == 0) or (not self.activeCommunity.active and self.chats.chats.len == 0)): return + + let selectedChannel = + if (self.activeCommunity.active): + self.activeCommunity.chats.getChannel(index) + else: + self.chats.getChannel(index) + if(not self.activeChannel.chatItem.isNil and self.activeChannel.chatItem.unviewedMessagesCount > 0): var response = self.status.chat.markAllChannelMessagesRead(self.activeChannel.id) if not response.hasKey("error"): self.chats.clearUnreadMessagesCount(self.activeChannel.chatItem) - let selectedChannel = self.chats.getChannel(index) + if self.activeChannel.id == selectedChannel.id: return if selectedChannel.chatType.isOneToOne and selectedChannel.id == selectedChannel.name: @@ -215,17 +232,28 @@ QtObject: self.activeChannel.setChatItem(selectedChannel) self.status.chat.setActiveChannel(selectedChannel.id) - proc getActiveChannelIdx(self: ChatsView): QVariant {.slot.} = - newQVariant(self.chats.chats.findIndexById(self.activeChannel.id)) + proc getActiveChannelIdx(self: ChatsView): int {.slot.} = + if (self.activeCommunity.active): + return self.activeCommunity.chats.chats.findIndexById(self.activeChannel.id) + else: + return self.chats.chats.findIndexById(self.activeChannel.id) - QtProperty[QVariant] activeChannelIndex: + QtProperty[int] activeChannelIndex: read = getActiveChannelIdx write = setActiveChannelByIndex notify = activeChannelChanged proc setActiveChannel*(self: ChatsView, channel: string) {.slot.} = if(channel == ""): return - self.activeChannel.setChatItem(self.chats.getChannel(self.chats.chats.findIndexById(channel))) + + let selectedChannel = + if (self.activeCommunity.active): + self.activeCommunity.chats.getChannel(self.activeCommunity.chats.chats.findIndexById(channel)) + else: + self.chats.getChannel(self.chats.chats.findIndexById(channel)) + + self.activeChannel.setChatItem(selectedChannel) + discard self.status.chat.markAllChannelMessagesRead(self.activeChannel.id) self.setLastMessageTimestamp(true) self.activeChannelChanged() @@ -547,3 +575,120 @@ QtObject: QtProperty[QVariant] transactions: read = getTransactions + proc communitiesChanged*(self: ChatsView) {.signal.} + + proc getCommunitiesIfNotFetched*(self: ChatsView): CommunityList = + if (not self.communityList.fetched): + let communities = self.status.chat.getAllComunities() + self.communityList.setNewData(communities) + self.communityList.fetched = true + return self.communityList + + proc getComunities*(self: ChatsView): QVariant {.slot.} = + return newQVariant(self.getCommunitiesIfNotFetched()) + + QtProperty[QVariant] communities: + read = getComunities + notify = communitiesChanged + + proc joinedCommunitiesChanged*(self: ChatsView) {.signal.} + + proc getJoinedComunities*(self: ChatsView): QVariant {.slot.} = + if (not self.joinedCommunityList.fetched): + let communities = self.status.chat.getJoinedComunities() + self.joinedCommunityList.setNewData(communities) + self.joinedCommunityList.fetched = true + + return newQVariant(self.joinedCommunityList) + + QtProperty[QVariant] joinedCommunities: + read = getJoinedComunities + notify = joinedCommunitiesChanged + + proc createCommunity*(self: ChatsView, name: string, description: string, color: string, imagePath: string): string {.slot.} = + result = "" + try: + # TODO Change this to get it from the user choices + let access = ord(CommunityAccessLevel.public) + let tmpImagePath = self.resizeImage(imagePath, 120) + let community = self.status.chat.createCommunity(name, description, color, tmpImagePath, access) + removeFile(tmpImagePath) + + if (community.id == ""): + return "Community was not created. Please try again later" + + self.communityList.addCommunityItemToList(community) + self.joinedCommunityList.addCommunityItemToList(community) + self.communitiesChanged() + except Exception as e: + error "Error creating the community", msg = e.msg + result = fmt"Error creating the community: {e.msg}" + + proc createCommunityChannel*(self: ChatsView, communityId: string, name: string, description: string): string {.slot.} = + result = "" + try: + let chat = self.status.chat.createCommunityChannel(communityId, name, description) + + if (chat.id == ""): + return "Chat was not created. Please try again later" + + self.joinedCommunityList.addChannelToCommunity(communityId, chat) + discard self.activeCommunity.chats.addChatItemToList(chat) + except Exception as e: + error "Error creating the channel", msg = e.msg + result = fmt"Error creating the channel: {e.msg}" + + proc activeCommunityChanged*(self: ChatsView) {.signal.} + + proc setActiveCommunity*(self: ChatsView, communityId: string) {.slot.} = + if(communityId == ""): return + self.activeCommunity.setCommunityItem(self.joinedCommunityList.getCommunityById(communityId)) + self.activeCommunity.setActive(true) + self.activeCommunityChanged() + + proc getActiveCommunity*(self: ChatsView): QVariant {.slot.} = + newQVariant(self.activeCommunity) + + QtProperty[QVariant] activeCommunity: + read = getActiveCommunity + write = setActiveCommunity + notify = activeCommunityChanged + + proc observedCommunityChanged*(self: ChatsView) {.signal.} + + proc setObservedCommunity*(self: ChatsView, communityId: string) {.slot.} = + if(communityId == ""): return + self.observedCommunity.setCommunityItem(self.communityList.getCommunityById(communityId)) + self.observedCommunityChanged() + + proc getObservedCommunity*(self: ChatsView): QVariant {.slot.} = + newQVariant(self.observedCommunity) + + QtProperty[QVariant] observedCommunity: + read = getObservedCommunity + write = setObservedCommunity + notify = observedCommunityChanged + + proc joinCommunity*(self: ChatsView, communityId: string): string {.slot.} = + result = "" + try: + self.status.chat.joinCommunity(communityId) + self.joinedCommunityList.addCommunityItemToList(self.communityList.getCommunityById(communityId)) + self.setActiveCommunity(communityId) + except Exception as e: + error "Error joining the community", msg = e.msg + result = fmt"Error joining the community: {e.msg}" + + proc leaveCommunity*(self: ChatsView, communityId: string): string {.slot.} = + result = "" + try: + self.status.chat.leaveCommunity(communityId) + if (communityId == self.activeCommunity.communityItem.id): + self.activeCommunity.setActive(false) + self.joinedCommunityList.removeCommunityItemFromList(communityId) + except Exception as e: + error "Error leaving the community", msg = e.msg + result = fmt"Error leaving the community: {e.msg}" + + proc leaveCurrentCommunity*(self: ChatsView): string {.slot.} = + result = self.leaveCommunity(self.activeCommunity.communityItem.id) diff --git a/src/app/chat/views/channels_list.nim b/src/app/chat/views/channels_list.nim index 7535da1d20..0a3f2a00c6 100644 --- a/src/app/chat/views/channels_list.nim +++ b/src/app/chat/views/channels_list.nim @@ -18,6 +18,7 @@ type ContentType = UserRole + 9 Muted = UserRole + 10 Id = UserRole + 11 + Description = UserRole + 12 QtObject: type @@ -75,6 +76,7 @@ QtObject: of ChannelsRoles.HasMentions: result = newQVariant(chatItem.hasMentions) of ChannelsRoles.Muted: result = newQVariant(chatItem.muted.bool) of ChannelsRoles.Id: result = newQVariant($chatItem.id) + of ChannelsRoles.Description: result = newQVariant(chatItem.description) method roleNames(self: ChannelsList): Table[int, string] = { @@ -88,9 +90,15 @@ QtObject: ChannelsRoles.HasMentions.int: "hasMentions", ChannelsRoles.ContentType.int: "contentType", ChannelsRoles.Muted.int: "muted", - ChannelsRoles.Id.int: "id" + ChannelsRoles.Id.int: "id", + ChannelsRoles.Description.int: "description" }.toTable + proc setChats*(self: ChannelsList, chats: seq[Chat]) = + self.beginResetModel() + self.chats = chats + self.endResetModel() + proc addChatItemToList*(self: ChannelsList, channel: Chat): int = self.beginInsertRows(newQModelIndex(), 0, 0) self.chats.insert(channel, 0) @@ -149,7 +157,7 @@ QtObject: else: self.chats[0] = channel - self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int]) + self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.Description.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int]) proc clearUnreadMessagesCount*(self: ChannelsList, channel: var Chat) = let idx = self.chats.findIndexById(channel.id) @@ -161,7 +169,7 @@ QtObject: channel.hasMentions = false self.chats[idx] = channel - self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int]) + self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.Description.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int]) proc renderInline(self: ChannelsList, elem: TextItem): string = case elem.textType: diff --git a/src/app/chat/views/community_item.nim b/src/app/chat/views/community_item.nim new file mode 100644 index 0000000000..45859c286d --- /dev/null +++ b/src/app/chat/views/community_item.nim @@ -0,0 +1,85 @@ +import NimQml, Tables, std/wrapnils +import ../../../status/[chat/chat, status] +import channels_list +import ../../../eventemitter + +QtObject: + type CommunityItemView* = ref object of QObject + communityItem*: Community + chats*: ChannelsList + status*: Status + active*: bool + + proc setup(self: CommunityItemView) = + self.QObject.setup + + proc delete*(self: CommunityItemView) = + if not self.chats.isNil: self.chats.delete + self.QObject.delete + + proc newCommunityItemView*(status: Status): CommunityItemView = + new(result, delete) + result = CommunityItemView() + result.status = status + result.active = false + result.chats = newChannelsList(status) + result.setup + + proc setCommunityItem*(self: CommunityItemView, communityItem: Community) = + self.communityItem = communityItem + self.chats.setChats(communityItem.chats) + + proc activeChanged*(self: CommunityItemView) {.signal.} + + proc setActive*(self: CommunityItemView, value: bool) {.slot.} = + self.active = value + self.status.events.emit("communityActiveChanged", Args()) + self.activeChanged() + + proc active*(self: CommunityItemView): bool {.slot.} = result = ?.self.active + + QtProperty[bool] active: + read = active + write = setActive + notify = activeChanged + + proc id*(self: CommunityItemView): string {.slot.} = result = ?.self.communityItem.id + + QtProperty[string] id: + read = id + + proc name*(self: CommunityItemView): string {.slot.} = result = ?.self.communityItem.name + + QtProperty[string] name: + read = name + + proc description*(self: CommunityItemView): string {.slot.} = result = ?.self.communityItem.description + + QtProperty[string] description: + read = description + + proc access*(self: CommunityItemView): int {.slot.} = result = ?.self.communityItem.access + + QtProperty[int] access: + read = access + + proc admin*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.admin + + QtProperty[bool] admin: + read = admin + + proc joined*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.joined + + QtProperty[bool] joined: + read = joined + + proc verified*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.verified + + QtProperty[bool] verified: + read = verified + + proc getChats*(self: CommunityItemView): QVariant {.slot.} = + result = newQVariant(self.chats) + + QtProperty[QVariant] chats: + read = getChats diff --git a/src/app/chat/views/community_list.nim b/src/app/chat/views/community_list.nim new file mode 100644 index 0000000000..542faf025b --- /dev/null +++ b/src/app/chat/views/community_list.nim @@ -0,0 +1,97 @@ +import NimQml, Tables +import ../../../status/chat/[chat, message] +import ../../../status/status +import ../../../status/ens +import ../../../status/accounts +import strutils + +type + CommunityRoles {.pure.} = enum + Id = UserRole + 1, + Name = UserRole + 2 + Description = UserRole + 3 + # Color = UserRole + 4 + Access = UserRole + 5 + Admin = UserRole + 6 + Joined = UserRole + 7 + Verified = UserRole + 8 + +QtObject: + type + CommunityList* = ref object of QAbstractListModel + communities*: seq[Community] + status: Status + fetched*: bool + + proc setup(self: CommunityList) = self.QAbstractListModel.setup + + proc delete(self: CommunityList) = + self.communities = @[] + self.QAbstractListModel.delete + + proc newCommunityList*(status: Status): CommunityList = + new(result, delete) + result.communities = @[] + result.status = status + result.setup() + + method rowCount*(self: CommunityList, index: QModelIndex = nil): int = self.communities.len + + method data(self: CommunityList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.communities.len: + return + + let communityItem = self.communities[index.row] + let communityItemRole = role.CommunityRoles + case communityItemRole: + of CommunityRoles.Name: result = newQVariant(communityItem.name) + of CommunityRoles.Description: result = newQVariant(communityItem.description) + of CommunityRoles.Id: result = newQVariant(communityItem.id) + # of CommunityRoles.Color: result = newQVariant(communityItem.color) + of CommunityRoles.Access: result = newQVariant(communityItem.access.int) + of CommunityRoles.Admin: result = newQVariant(communityItem.admin.bool) + of CommunityRoles.Joined: result = newQVariant(communityItem.joined.bool) + of CommunityRoles.Verified: result = newQVariant(communityItem.verified.bool) + + method roleNames(self: CommunityList): Table[int, string] = + { + CommunityRoles.Name.int:"name", + CommunityRoles.Description.int:"description", + CommunityRoles.Id.int: "id", + # CommunityRoles.Color.int: "color", + CommunityRoles.Access.int: "access", + CommunityRoles.Admin.int: "admin", + CommunityRoles.Verified.int: "verified", + CommunityRoles.Joined.int: "joined" + }.toTable + + proc setNewData*(self: CommunityList, communityList: seq[Community]) = + self.beginResetModel() + self.communities = communityList + self.endResetModel() + + proc addCommunityItemToList*(self: CommunityList, community: Community) = + self.beginInsertRows(newQModelIndex(), self.communities.len, self.communities.len) + self.communities.add(community) + self.endInsertRows() + + proc removeCommunityItemFromList*(self: CommunityList, id: string) = + let idx = self.communities.findIndexById(id) + self.beginRemoveRows(newQModelIndex(), idx, idx) + self.communities.delete(idx) + self.endRemoveRows() + + proc getCommunityById*(self: CommunityList, communityId: string): Community = + for community in self.communities: + if community.id == communityId: + return community + +proc addChannelToCommunity*(self: CommunityList, communityId: string, chat: Chat) = + var community = self.getCommunityById(communityId) + community.chats.add(chat) + + let index = self.communities.findIndexById(communityId) + self.communities[index] = community + \ No newline at end of file diff --git a/src/status/chat.nim b/src/status/chat.nim index 6e4318b8b4..4b63acaf6a 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -373,3 +373,21 @@ proc requestTransaction*(self: ChatModel, chatId: string, fromAddress: string, a let address = if (tokenAddress == constants.ZERO_ADDRESS): "" else: tokenAddress let response = status_chat_commands.requestTransaction(chatId, fromAddress, amount, address) discard self.processMessageUpdateAfterSend(response) + +proc getAllComunities*(self: ChatModel): seq[Community] = + result = status_chat.getAllComunities() + +proc getJoinedComunities*(self: ChatModel): seq[Community] = + result = status_chat.getJoinedComunities() + +proc createCommunity*(self: ChatModel, name: string, description: string, color: string, image: string, access: int): Community = + result = status_chat.createCommunity(name, description, color, image, access) + +proc createCommunityChannel*(self: ChatModel, communityId: string, name: string, description: string): Chat = + result = status_chat.createCommunityChannel(communityId, name, description) + +proc joinCommunity*(self: ChatModel, communityId: string) = + status_chat.joinCommunity(communityId) + +proc leaveCommunity*(self: ChatModel, communityId: string) = + status_chat.leaveCommunity(communityId) \ No newline at end of file diff --git a/src/status/chat/chat.nim b/src/status/chat/chat.nim index 0f0d438e00..31c91fad4e 100644 --- a/src/status/chat/chat.nim +++ b/src/status/chat/chat.nim @@ -8,6 +8,7 @@ type ChatType* {.pure.}= enum PrivateGroupChat = 3, Profile = 4, Timeline = 5 + CommunityChat = 6 proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne proc isTimeline*(self: ChatType): bool = self == ChatType.Timeline @@ -58,6 +59,7 @@ proc toJsonNode*(self: seq[ChatMembershipEvent]): seq[JsonNode] = type Chat* = ref object id*: string # ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one is the hex encoded public key and for group chats is a random uuid appended with the hex encoded pk of the creator of the chat name*: string + description*: string color*: string identicon*: string isActive*: bool # indicates whether the chat has been soft deleted @@ -73,9 +75,30 @@ type Chat* = ref object muted*: bool ensName*: string +type CommunityAccessLevel* = enum + unknown = 0 + public = 1 + invitationOnly = 2 + onRequest = 3 + +type Community* = object + id*: string + name*: string + description*: string + chats*: seq[Chat] + # members: seq[] # TODO find what goes in there + # color*: string + access*: int + admin*: bool + joined*: bool + verified*: bool + proc `$`*(self: Chat): string = result = fmt"Chat(id:{self.id}, name:{self.name}, active:{self.isActive}, type:{self.chatType})" +proc `$`*(self: Community): string = + result = fmt"Community(id:{self.id}, name:{self.name}, description:{self.description}" + proc toJsonNode*(self: Chat): JsonNode = result = %* { "active": self.isActive, @@ -101,6 +124,15 @@ proc findIndexById*(self: seq[Chat], id: string): int = result = idx break +proc findIndexById*(self: seq[Community], id: string): int = + result = -1 + var idx = -1 + for item in self: + inc idx + if(item.id == id): + result = idx + break + proc isMember*(self: Chat, pubKey: string): bool = for member in self.members: if member.id == pubKey and member.joined: return true diff --git a/src/status/chat/message.nim b/src/status/chat/message.nim index 7a7bac12d7..891cf82f30 100644 --- a/src/status/chat/message.nim +++ b/src/status/chat/message.nim @@ -12,6 +12,7 @@ type ContentType* {.pure.} = enum Group = 6, Image = 7, Audio = 8 + Community = 9 type TextItem* = object textType*: string diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index 4aa6521473..9b9281c626 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -51,8 +51,8 @@ proc loadChats*(): seq[Chat] = if jsonResponse["result"].kind != JNull: for jsonChat in jsonResponse{"result"}: let chat = jsonChat.toChat - if chat.isActive and chat.chatType != ChatType.Unknown: - result.add(jsonChat.toChat) + if chat.isActive and chat.chatType != ChatType.Unknown and chat.chatType != ChatType.CommunityChat: + result.add(chat) result.sort(sortChats) proc chatMessages*(chatId: string, cursor: string = ""): (string, seq[Message]) = @@ -209,3 +209,90 @@ proc getLinkPreviewData*(link: string): JsonNode = raise newException(RpcException, fmt"""Error getting link preview data for '{link}': {response.error.message}""") response.result + + +proc getAllComunities*(): seq[Community] = + var communities: seq[Community] = @[] + let rpcResult = callPrivateRPC("communities".prefix).parseJSON() + debug "LIST", rpcResult + if rpcResult{"result"}.kind != JNull: + for jsonCommunity in rpcResult["result"]: + var community = jsonCommunity.toCommunity() + + communities.add(community) + return communities + +proc getJoinedComunities*(): seq[Community] = + var communities: seq[Community] = @[] + let rpcResult = callPrivateRPC("joinedCommunities".prefix).parseJSON() + debug "LIST", rpcResult + if rpcResult{"result"}.kind != JNull: + for jsonCommunity in rpcResult["result"]: + var community = jsonCommunity.toCommunity() + + communities.add(community) + return communities + +proc createCommunity*(name: string, description: string, color: string, image: string, access: int): Community = + let rpcResult = callPrivateRPC("createCommunity".prefix, %*[{ + "permissions": { + "access": access + }, + "identity": { + "display_name": name, + "description": description#, + # "color": color#, + # TODO add images once it is supported by Status-Go + # "images": [ + # { + # "payload": image, + # # TODO get that from an enum + # "image_type": 1 # 1 is a raw payload + # } + # ] + } + }]).parseJSON() + debug "RESULT", rpcResult + + if rpcResult{"result"}.kind != JNull: + result = rpcResult["result"]["communities"][0].toCommunity() + +proc createCommunityChannel*(communityId: string, name: string, description: string): Chat = + let rpcResult = callPrivateRPC("createCommunityChat".prefix, %*[ + communityId, + { + "permissions": { + "access": 1 # TODO get this from user selected privacy setting + }, + "identity": { + "display_name": name, + "description": description#, + # "color": color#, + # TODO add images once it is supported by Status-Go + # "images": [ + # { + # "payload": image, + # # TODO get that from an enum + # "image_type": 1 # 1 is a raw payload + # } + # ] + } + }]).parseJSON() + debug "RESULT", rpcResult + + if rpcResult{"result"}.kind != JNull: + result = rpcResult["result"]["chats"][0].toChat() + + +#{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":{\"chats\":[{\"id\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20cbf03afb-89ab-444e-96e9-e23b6c1266c0\",\"name\":\"general\",\"color\":\"#887af9\",\"active\":true,\"chatType\":6,\"timestamp\":1606424570375,\"lastClockValue\":0,\"deletedAtClockValue\":0,\"unviewedMessagesCount\":0,\"lastMessage\":null,\"members\":null,\"membershipUpdateEvents\":null,\"identicon\":\"\",\"communityId\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20\"}],\"communities\":[{\"id\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20\",\"description\":{\"clock\":2,\"permissions\":{\"access\":1},\"identity\":{\"display_name\":\"Jo2\",\"description\":\"Jo again\"},\"chats\":{\"cbf03afb-89ab-444e-96e9-e23b6c1266c0\":{\"permissions\":{\"access\":1},\"identity\":{\"display_name\":\"general\",\"description\":\"general channel\"}}}},\"admin\":true,\"verified\":false,\"joined\":true}],\"communitiesChanges\":[{\"MembersAdded\":{},\"MembersRemoved\":{},\"ChatsRemoved\":{},\"ChatsAdded\":{\"cbf03afb-89ab-444e-96e9-e23b6c1266c0\":{\"permissions\":{\"access\":1},\"identity\":{\"display_name\":\"general\",\"description\":\"general channel\"}}},\"ChatsModified\":{}}],\"filters\":[{\"chatId\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20cbf03afb-89ab-444e-96e9-e23b6c1266c0\",\"filterId\":\"eb220a77bde14d967462529d0ec7c1fa09a0ece4efebefb388ea6e0ecf9a1950\",\"symKeyId\":\"0f08b999ab6571429f79c4762bd922f2b7db38c80ba99cd4a5ac15ef75357d0e\",\"oneToOne\":false,\"identity\":\"\",\"topic\":\"0x46a1c2be\",\"discovery\":false,\"negotiated\":false,\"listen\":true}]}} + + # if rpcResult{"result"}.kind != JNull: + # result = rpcResult["result"]["communities"][0].toCommunity() + +proc joinCommunity*(communityId: string) = + let res = callPrivateRPC("joinCommunity".prefix, %*[communityId])#.parseJSON()["result"] + debug "RESULT", res + +proc leaveCommunity*(communityId: string) = + let res = callPrivateRPC("leaveCommunity".prefix, %*[communityId])#.parseJSON()["result"] + debug "RESULT", res \ No newline at end of file diff --git a/src/status/signals/messages.nim b/src/status/signals/messages.nim index 9b91c97459..ece2ad8d78 100644 --- a/src/status/signals/messages.nim +++ b/src/status/signals/messages.nim @@ -152,6 +152,31 @@ proc toChat*(jsonChat: JsonNode): Chat = for jsonMember in jsonChat["membershipUpdateEvents"]: result.membershipUpdateEvents.add(jsonMember.toChatMembershipEvent) +proc toCommunity*(jsonCommunity: JsonNode): Community = + result = Community( + id: jsonCommunity{"id"}.getStr, + name: jsonCommunity{"description"}{"identity"}{"display_name"}.getStr, + description: jsonCommunity{"description"}{"identity"}{"description"}.getStr, + # color: jsonCommunity{"description"}{"identity"}{"color"}.getStr, + access: jsonCommunity{"description"}{"permissions"}{"access"}.getInt, + admin: jsonCommunity{"admin"}.getBool, + joined: jsonCommunity{"joined"}.getBool, + verified: jsonCommunity{"verified"}.getBool, + chats: newSeq[Chat]() + ) + + if not jsonCommunity["description"].hasKey("chats") or jsonCommunity["description"]["chats"].kind == JNull: + return result + + for chatId, chat in jsonCommunity{"description"}{"chats"}: + result.chats.add(Chat( + id: chatId, + name: chat{"identity"}{"display_name"}.getStr, + description: chat{"identity"}{"description"}.getStr, + # TODO get this from access + chatType: ChatType.Public#chat{"permissions"}{"access"}.getInt, + )) + proc toTextItem*(jsonText: JsonNode): TextItem = result = TextItem( literal: jsonText{"literal"}.getStr, diff --git a/ui/app/AppLayouts/Chat/ChatLayout.qml b/ui/app/AppLayouts/Chat/ChatLayout.qml index 81622c133f..f6bc543c5c 100644 --- a/ui/app/AppLayouts/Chat/ChatLayout.qml +++ b/ui/app/AppLayouts/Chat/ChatLayout.qml @@ -17,6 +17,12 @@ SplitView { chatColumn.onActivated() } + function openPopup(popupComponent, params = {}) { + const popup = popupComponent.createObject(chatView, params); + popup.open() + return popup + } + Connections { target: applicationWindow onSettingsLoaded: { @@ -26,16 +32,32 @@ SplitView { } Component.onDestruction: appSettings.chatSplitView = this.saveState() - ContactsColumn { - id: contactsColumn - SplitView.preferredWidth: Style.current.leftTabPrefferedSize - SplitView.minimumWidth: Style.current.leftTabMinimumWidth - SplitView.maximumWidth: Style.current.leftTabMaximumWidth + Loader { + id: contactColumnLoader + sourceComponent: chatsModel.activeCommunity.active ? communtiyColumnComponent : contactsColumnComponent + } + + Component { + id: contactsColumnComponent + ContactsColumn { + SplitView.preferredWidth: Style.current.leftTabPrefferedSize + SplitView.minimumWidth: Style.current.leftTabMinimumWidth + SplitView.maximumWidth: Style.current.leftTabMaximumWidth + } + } + + Component { + id: communtiyColumnComponent + CommunityColumn { + SplitView.preferredWidth: Style.current.leftTabPrefferedSize + SplitView.minimumWidth: Style.current.leftTabMinimumWidth + SplitView.maximumWidth: Style.current.leftTabMaximumWidth + } } ChatColumn { id: chatColumn - chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount + chatGroupsListViewCount: contactColumnLoader.item.chatGroupsListViewCount } function openProfilePopup(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, parentPopup){ diff --git a/ui/app/AppLayouts/Chat/CommunityColumn.qml b/ui/app/AppLayouts/Chat/CommunityColumn.qml new file mode 100644 index 0000000000..ae7ea5f176 --- /dev/null +++ b/ui/app/AppLayouts/Chat/CommunityColumn.qml @@ -0,0 +1,147 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 + +import "../../../imports" +import "../../../shared" +import "../../../shared/status" +import "./ContactsColumn" +import "./ContactsColumn/CommunityComponents" + +Item { + // TODO unhardcode + property int chatGroupsListViewCount: channelList.channelListCount + + id: root + Layout.fillHeight: true + + Component { + id: createChannelPopup + CreateChannelPopup { + onClosed: { + destroy() + } + } + } + + Item { + id: communityHeader + width: parent.width + height: communityImage.height + anchors.top: parent.top + anchors.topMargin: Style.current.padding + + StatusIconButton { + id: backArrow + icon.name: "arrow-right" + iconRotation: 180 + iconColor: Style.current.inputColor + anchors.left: parent.left + anchors.leftMargin: Style.current.bigPadding + anchors.verticalCenter: parent.verticalCenter + onClicked: chatsModel.activeCommunity.active = false + } + + RoundedImage { + id: communityImage + width: 40 + height: 40 + // TODO get the real image once it's available + source: "../../img/ens-header-dark@2x.png" + anchors.left: backArrow.right + anchors.leftMargin: Style.current.smallPadding + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: communityName + text: chatsModel.activeCommunity.name + anchors.left: communityImage.right + anchors.leftMargin: Style.current.halfPadding + font.pixelSize: 15 + font.weight: Font.Medium + } + + StyledText { + id: communityNbMember + // TOD get real numbers + text: qsTr("%1 members").arg(12) + anchors.left: communityName.left + anchors.bottom: parent.bottom + font.pixelSize: 12 + font.weight: Font.Thin + color: Style.current.secondaryText + } + + StatusIconButton { + id: optionsBtn + icon.name: "dots-icon" + iconColor: Style.current.inputColor + anchors.right: parent.right + anchors.rightMargin: Style.current.bigPadding + anchors.verticalCenter: parent.verticalCenter + onClicked: { + optionsMenu.open() + } + } + + PopupMenu { + id: optionsMenu + x: optionsBtn.x + optionsBtn.width / 2 - optionsMenu.width / 2 + y: optionsBtn.height + + Action { + enabled: chatsModel.activeCommunity.admin + text: qsTrId("Create channel") + icon.source: "../../img/hash.svg" + icon.width: 20 + icon.height: 20 + onTriggered: openPopup(createChannelPopup, {communityId: chatsModel.activeCommunity.id}) + } + + Action { + text: qsTrId("Leave community") + icon.source: "../../img/delete.svg" + icon.color: Style.current.red + icon.width: 20 + icon.height: 20 + onTriggered: chatsModel.leaveCurrentCommunity() + } + } + } + + + ScrollView { + id: chatGroupsContainer + anchors.top: communityHeader.bottom + anchors.topMargin: Style.current.padding + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + leftPadding: Style.current.halfPadding + rightPadding: Style.current.halfPadding + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentHeight: channelList.height + emptyViewAndSuggestions.height + 2 * Style.current.padding + clip: true + + ChannelList { + id: channelList + searchStr: "" + channelModel: chatsModel.activeCommunity.chats + } + + CommunityWelcomeBanner { + id: emptyViewAndSuggestions + visible: chatsModel.activeCommunity.admin + width: parent.width + anchors.top: channelList.bottom + anchors.topMargin: Style.current.padding + } + } +} + +/*##^## +Designer { + D{i:0;autoSize:true;formeditorColor:"#ffffff";height:480;width:640} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/ContactsColumn.qml b/ui/app/AppLayouts/Chat/ContactsColumn.qml index 5425a0c052..b1d006d0cd 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn.qml @@ -25,16 +25,58 @@ Item { font.pixelSize: 17 } - PublicChatPopup { - id: publicChatPopup + Component { + id: publicChatPopupComponent + PublicChatPopup { + onClosed: { + destroy() + } + } } - GroupChatPopup { - id: groupChatPopup + Component { + id: groupChatPopupComponent + GroupChatPopup { + onClosed: { + destroy() + } + } } - PrivateChatPopup { - id: privateChatPopup + Component { + id: privateChatPopupComponent + PrivateChatPopup { + onClosed: { + destroy() + } + } + } + + Component { + id: communitiesPopupComponent + CommunitiesPopup { + onClosed: { + destroy() + } + } + } + + Component { + id: createCommunitiesPopupComponent + CreateCommunityPopup { + onClosed: { + destroy() + } + } + } + + Component { + id: communityDetailPopup + CommunityDetailPopup { + onClosed: { + destroy() + } + } } SearchBox { @@ -55,15 +97,45 @@ Item { anchors.topMargin: Style.current.padding } - ChannelList { - id: channelList - searchStr: contactsColumn.searchStr - + ScrollView { + id: chatGroupsContainer + anchors.top: searchBox.bottom + anchors.topMargin: Style.current.padding anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.top: searchBox.bottom - anchors.topMargin: Style.current.padding + leftPadding: Style.current.halfPadding + rightPadding: Style.current.halfPadding + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentHeight: communityList.height + channelList.height + 2 * Style.current.padding + emptyViewAndSuggestions.height + clip: true + + CommunityList { + id: communityList + searchStr: contactsColumn.searchStr + } + + Separator { + id: communitySep + visible: communityList.visible + anchors.top: communityList.bottom + anchors.topMargin: Style.current.halfPadding + } + + ChannelList { + id: channelList + anchors.top: communitySep.bottom + anchors.topMargin: Style.current.halfPadding + searchStr: contactsColumn.searchStr + channelModel: chatsModel.chats + } + + EmptyView { + id: emptyViewAndSuggestions + width: parent.width + anchors.top: channelList.bottom + anchors.topMargin: Style.current.smallPadding + } } } diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml b/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml index a96f4e6d0d..fa286af048 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn/AddChat.qml @@ -27,7 +27,7 @@ StatusRoundButton { icon.source: "../../../img/new_chat.svg" icon.width: 20 icon.height: 20 - onTriggered: privateChatPopup.open() + onTriggered: openPopup(groupChatPopupComponent) } Action { //% "Start group chat" @@ -35,7 +35,7 @@ StatusRoundButton { icon.source: "../../../img/group_chat.svg" icon.width: 20 icon.height: 20 - onTriggered: groupChatPopup.open() + onTriggered: openPopup(groupChatPopupComponent) } Action { //% "Join public chat" @@ -43,7 +43,16 @@ StatusRoundButton { icon.source: "../../../img/public_chat.svg" icon.width: 20 icon.height: 20 - onTriggered: publicChatPopup.open() + onTriggered: openPopup(publicChatPopupComponent) + } + Action { + text: qsTr("Communities") + icon.source: "../../../img/communities.svg" + icon.width: 20 + icon.height: 20 + onTriggered: { + openPopup(communitiesPopupComponent) + } } onAboutToHide: { btnAdd.state = "default" diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/Channel.qml b/ui/app/AppLayouts/Chat/ContactsColumn/Channel.qml index 4290bd0818..d9ab94b82b 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn/Channel.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn/Channel.qml @@ -9,7 +9,7 @@ Rectangle { property string chatId: "" property string name: "channelName" property string lastMessage: "My latest message\n with a return" - property string timestamp: "20/2/2020" + property string timestamp: "1605212622434" property string unviewedMessagesCount: "2" property string identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQAQMAAAC6caSPAAAABlBMVEXMzMz////TjRV2AAAAAWJLR0QB/wIt3gAAACpJREFUGBntwYEAAAAAw6D7Uw/gCtUAAAAAAAAAAAAAAAAAAAAAAAAAgBNPsAABAjKCqQAAAABJRU5ErkJggg==" property bool hasMentions: false diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/ChannelList.qml b/ui/app/AppLayouts/Chat/ContactsColumn/ChannelList.qml index fcec17f467..953d9c556a 100644 --- a/ui/app/AppLayouts/Chat/ContactsColumn/ChannelList.qml +++ b/ui/app/AppLayouts/Chat/ContactsColumn/ChannelList.qml @@ -6,93 +6,73 @@ import "../../../../imports" import "../components" import "./" -ScrollView { +Rectangle { + property var channelModel property alias channelListCount: chatGroupsListView.count property string searchStr: "" - id: chatGroupsContainer - Layout.fillHeight: true - Layout.fillWidth: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentHeight: channelListContent.height + Style.current.padding - clip: true + id: channelListContent + width: parent.width + height: childrenRect.height Timer { id: timer } - - Item { - id: channelListContent - Layout.fillHeight: true + ListView { + id: chatGroupsListView + spacing: Style.current.halfPadding anchors.top: parent.top + height: childrenRect.height + visible: height > 0 anchors.right: parent.right anchors.left: parent.left - height: childrenRect.height - - ListView { - id: chatGroupsListView - anchors.top: parent.top - height: childrenRect.height - visible: height > 0 - anchors.right: parent.right - anchors.left: parent.left - anchors.rightMargin: Style.current.padding - anchors.leftMargin: Style.current.padding - interactive: false - model: chatsModel.chats - delegate: Channel { - name: model.name - muted: model.muted - lastMessage: model.lastMessage - timestamp: model.timestamp - chatType: model.chatType - identicon: model.identicon - unviewedMessagesCount: model.unviewedMessagesCount - hasMentions: model.hasMentions - contentType: model.contentType - searchStr: chatGroupsContainer.searchStr - chatId: model.id - } - onCountChanged: { - if (count > 0 && chatsModel.activeChannelIndex > -1) { - // If a chat is added or removed, we set the current index to the first value - chatsModel.activeChannelIndex = 0; - currentIndex = 0; + interactive: false + model: channelListContent.channelModel + delegate: Channel { + name: model.name + muted: model.muted + lastMessage: model.lastMessage + timestamp: model.timestamp + chatType: model.chatType + identicon: model.identicon + unviewedMessagesCount: model.unviewedMessagesCount + hasMentions: model.hasMentions + contentType: model.contentType + searchStr: channelListContent.searchStr + chatId: model.id + } + onCountChanged: { + if (count > 0 && chatsModel.activeChannelIndex > -1) { + // If a chat is added or removed, we set the current index to the first value + chatsModel.activeChannelIndex = 0; + currentIndex = 0; + } else { + if (chatsModel.activeChannelIndex > -1) { + chatGroupsListView.currentIndex = 0; } else { - if(chatsModel.activeChannelIndex > -1){ - chatGroupsListView.currentIndex = 0; - } else { - // Initial state. No chat has been selected yet - chatGroupsListView.currentIndex = -1; - } + // Initial state. No chat has been selected yet + chatGroupsListView.currentIndex = -1; } } } + } - Rectangle { - id: noSearchResults - anchors.top: parent.top - height: 300 - color: "transparent" - visible: !chatGroupsListView.visible && chatGroupsContainer.searchStr !== "" - anchors.left: parent.left - anchors.right: parent.right + Rectangle { + id: noSearchResults + anchors.top: parent.top + height: visible ? 300 : 0 + color: "transparent" + visible: !chatGroupsListView.visible && channelListContent.searchStr !== "" + anchors.left: parent.left + anchors.right: parent.right - StyledText { - font.pixelSize: 15 - color: Style.current.darkGrey - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("No search results") - } + StyledText { + font.pixelSize: 15 + color: Style.current.darkGrey + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("No search results") } - - EmptyView { - width: parent.width - anchors.top: noSearchResults.visible ? noSearchResults.bottom : chatGroupsListView.bottom - anchors.topMargin: Style.current.smallPadding - } - } GroupInfoPopup { @@ -252,7 +232,10 @@ ScrollView { chatColumn.isReply = false; } } + } + + /*##^## Designer { D{i:0;autoSize:true;height:480;width:640} diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml new file mode 100644 index 0000000000..63f6433535 --- /dev/null +++ b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml @@ -0,0 +1,99 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import "../../../../shared" +import "../../../../shared/status" +import "../../../../imports" +import "../components" + +Rectangle { + property string communityId: "" + property string name: "channelName" + property string unviewedMessagesCount: "2" + property string image: "../../../img/ens-header-dark@2x.png" + property bool hasMentions: false + property string searchStr: "" + property bool isCompact: appSettings.compactMode + property bool hovered: false + + id: wrapper + color: { + if (wrapper.hovered) { + return Style.current.secondaryBackground + } + return Style.current.transparent + } + anchors.right: parent.right + anchors.top: applicationWindow.top + anchors.left: parent.left + radius: Style.current.radius + // Hide the box if it is filtered out + property bool isVisible: searchStr === "" || name.includes(searchStr) + visible: isVisible ? true : false + height: isVisible ? !isCompact ? 64 : communityImage.height + Style.current.smallPadding * 2 : 0 + + RoundedImage { + id: communityImage + height: !isCompact ? 40 : 20 + width: !isCompact ? 40 : 20 + source: wrapper.image + anchors.left: parent.left + anchors.leftMargin: !isCompact ? Style.current.padding : Style.current.smallPadding + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + id: contactInfo + text: wrapper.name + anchors.right: contactNumberChatsCircle.left + anchors.rightMargin: Style.current.smallPadding + elide: Text.ElideRight + font.weight: Font.Medium + font.pixelSize: 15 + anchors.left: communityImage.right + anchors.leftMargin: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: contactNumberChatsCircle + width: 22 + height: 22 + radius: 50 + anchors.right: parent.right + anchors.rightMargin: !isCompact ? Style.current.padding : Style.current.smallPadding + anchors.verticalCenter: parent.verticalCenter + color: Style.current.primary + visible: (unviewedMessagesCount > 0) || wrapper.hasMentions + StyledText { + id: contactNumberChats + text: wrapper.hasMentions ? '@' : (wrapper.unviewedMessagesCount < 100 ? wrapper.unviewedMessagesCount : "99") + font.pixelSize: 12 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: Style.current.white + } + } + + MouseArea { + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + hoverEnabled: true + onEntered: { + wrapper.hovered = true + } + onExited: { + wrapper.hovered = false + } + onClicked: { + chatsModel.setActiveCommunity(communityId) + } + } + +} + +/*##^## +Designer { + D{i:0;formeditorColor:"#ffffff";height:64;width:640} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/CommunityComponents/CreateChannelPopup.qml b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityComponents/CreateChannelPopup.qml new file mode 100644 index 0000000000..61be29c49f --- /dev/null +++ b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityComponents/CreateChannelPopup.qml @@ -0,0 +1,149 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtGraphicalEffects 1.13 +import QtQuick.Dialogs 1.3 +import "../../../../../imports" +import "../../../../../shared" +import "../../../../../shared/status" + +ModalPopup { + property string communityId + readonly property int maxDescChars: 140 + property string nameValidationError: "" + + id: popup + height: 600 + + onOpened: { + nameInput.text = ""; + nameInput.forceActiveFocus(Qt.MouseFocusReason) + } + + function validate() { + nameValidationError = "" + + if (nameInput.text === "") { + nameValidationError = qsTr("You need to enter a name") + } else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) { + nameValidationError = qsTr("Please restrict your name to letters, numbers, dashes and spaces") + } else if (nameInput.text.length > 100) { + nameValidationError = qsTr("Your name needs to be 100 characters or shorter") + } + + return !nameValidationError && !descriptionTextArea.validationError + } + + title: qsTr("New channel") + + ScrollView { + property ScrollBar vScrollBar: ScrollBar.vertical + + id: scrollView + anchors.fill: parent + rightPadding: Style.current.padding + anchors.rightMargin: - Style.current.halfPadding + contentHeight: content.height + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + clip: true + + function scrollBackUp() { + vScrollBar.setPosition(0) + } + + Item { + id: content + height: childrenRect.height + width: parent.width + + Input { + id: nameInput + label: qsTr("Channel name") + placeholderText: qsTr("A cool name") + validationError: popup.nameValidationError + } + + StyledTextArea { + id: descriptionTextArea + label: qsTr("Channel description") + placeholderText: qsTr("What your channel is about") + validationError: descriptionTextArea.text.length > maxDescChars ? qsTr("The description cannot exceed %1 characters").arg(maxDescChars) : "" + anchors.top: nameInput.bottom + anchors.topMargin: Style.current.bigPadding + customHeight: 88 + } + + StyledText { + id: charLimit + text: `${descriptionTextArea.text.length}/${maxDescChars}` + anchors.top: descriptionTextArea.bottom + anchors.topMargin: !descriptionTextArea.validationError ? 5 : - Style.current.smallPadding + anchors.right: descriptionTextArea.right + font.pixelSize: 12 + color: !descriptionTextArea.validationError ? Style.current.textColor : Style.current.danger + } + + Separator { + id: separator1 + anchors.top: charLimit.bottom + anchors.topMargin: Style.current.bigPadding + } + + Item { + id: privateSwitcher + height: privateSwitch.height + width: parent.width + anchors.top: separator1.bottom + anchors.topMargin: Style.current.smallPadding * 2 + + StyledText { + text: qsTr("Private channel") + anchors.verticalCenter: parent.verticalCenter + } + + StatusSwitch { + id: privateSwitch + anchors.right: parent.right + } + } + + StyledText { + id: privateExplanation + anchors.top: privateSwitcher.bottom + wrapMode: Text.WordWrap + anchors.topMargin: Style.current.smallPadding * 2 + width: parent.width + text: qsTr("By making a channel private, only members with selected permission will be able to access it") + } + } + } + + footer: StatusButton { + text: qsTr("Create") + anchors.right: parent.right + onClicked: { + if (!validate()) { + scrollView.scrollBackUp() + return + } + const error = chatsModel.createCommunityChannel(communityId, + Utils.filterXSS(nameInput.text), + Utils.filterXSS(descriptionTextArea.text)) + + if (error) { + creatingError.text = error + return creatingError.open() + } + + // TODO Open the community once we have designs for it + popup.close() + } + + MessageDialog { + id: creatingError + title: qsTr("Error creating the community") + icon: StandardIcon.Critical + standardButtons: StandardButton.Ok + } + } +} + diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/CommunityList.qml b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityList.qml new file mode 100644 index 0000000000..c07b269683 --- /dev/null +++ b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityList.qml @@ -0,0 +1,78 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import "../../../../shared" +import "../../../../imports" +import "../components" +import "./" + + +Item { + property string searchStr: "" + id: root + width: parent.width + height: childrenRect.height + visible: communityListView.visible + + ListView { + id: communityListView + spacing: Style.current.halfPadding + anchors.top: parent.top + height: childrenRect.height + // FIXME the height doesn't update +// visible: height > 0 + width:parent.width + interactive: false + model: chatsModel.joinedCommunities + delegate: CommunityButton { + communityId: model.id + name: model.name + // TODO add other properties + searchStr: root.searchStr + } + } + + Item { + id: noSearchResults + anchors.top: parent.top + height: visible ? 200 : 0 + visible: !communityListView.visible && root.searchStr !== "" + width: parent.width + + StyledText { + font.pixelSize: 15 + color: Style.current.darkGrey + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("No search results in Communities") + } + } + +// Connections { +// target: chatsModel.chats +// onDataChanged: { +// // If the current active channel receives messages and changes its position, +// // refresh the currentIndex accordingly +// if(chatsModel.activeChannelIndex !== communityListView.currentIndex){ +// communityListView.currentIndex = chatsModel.activeChannelIndex +// } +// } +// } + +// Connections { +// target: chatsModel +// onActiveChannelChanged: { +// chatsModel.hideLoadingIndicator() +// communityListView.currentIndex = chatsModel.activeChannelIndex +// SelectedMessage.reset(); +// chatColumn.isReply = false; +// } +// } +} + + +/*##^## +Designer { + D{i:0;autoSize:true;height:480;width:640} +} +##^##*/ diff --git a/ui/app/AppLayouts/Chat/ContactsColumn/CommunityWelcomeBanner.qml b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityWelcomeBanner.qml new file mode 100644 index 0000000000..85ca622734 --- /dev/null +++ b/ui/app/AppLayouts/Chat/ContactsColumn/CommunityWelcomeBanner.qml @@ -0,0 +1,81 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtGraphicalEffects 1.13 +import "../../../../shared" +import "../../../../shared/status" +import "../../../../imports" + +Rectangle { + id: root + height: visible ? 220 : 0 + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + border.color: Style.current.border + radius: 16 + color: Style.current.transparent + + + SVGImage { + anchors.top: parent.top + anchors.topMargin: -6 + anchors.horizontalCenter: parent.horizontalCenter + source: "../../../img/chatEmptyHeader.svg" + width: 66 + height: 50 + } + + StatusIconButton { + icon.name: "close" + id: closeImg + anchors.top: parent.top + anchors.topMargin: 10 + anchors.right: parent.right + anchors.rightMargin: 10 + icon.height: 20 + icon.width: 20 + iconColor: Style.current.darkGrey + onClicked: { + // TODO make this saved in the settings + root.visible = false + } + } + + StyledText { + id: welcomeText + text: qsTr("Welcome to your community! ") + anchors.top: parent.top + anchors.topMargin: 60 + horizontalAlignment: Text.AlignHCenter + font.pixelSize: 15 + wrapMode: Text.WordWrap + anchors.right: parent.right + anchors.rightMargin: Style.current.xlPadding + anchors.left: parent.left + anchors.leftMargin: Style.current.xlPadding + } + + StatusButton { + id: addMembersBtn + text: qsTr("Add members") + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: manageBtn.top + anchors.bottomMargin: Style.current.halfPadding + onClicked: { + console.log('ADD') + } + } + + StatusButton { + id: manageBtn + text: qsTr("Manage community") + type: "secondary" + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.current.padding + onClicked: { + console.log('Manage') + } + } +} diff --git a/ui/app/AppLayouts/Chat/components/CommunitiesPopup.qml b/ui/app/AppLayouts/Chat/components/CommunitiesPopup.qml new file mode 100644 index 0000000000..8cc48fa71b --- /dev/null +++ b/ui/app/AppLayouts/Chat/components/CommunitiesPopup.qml @@ -0,0 +1,104 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.3 +import QtGraphicalEffects 1.13 +import "../../../../imports" +import "../../../../shared" +import "../../../../shared/status" + +ModalPopup { + id: popup + + onOpened: { + searchBox.text = ""; + searchBox.forceActiveFocus(Qt.MouseFocusReason) + } + + title: qsTr("Communities") + + SearchBox { + id: searchBox + iconWidth: 17 + iconHeight: 17 + customHeight: 36 + fontPixelSize: 15 + } + + ScrollView { + id: scrollView + width: parent.width + anchors.topMargin: Style.current.padding + anchors.top: searchBox.bottom + anchors.bottom: parent.bottom + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: communitiesList.contentHeight > communitiesList.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + + ListView { + anchors.fill: parent + model: chatsModel.communities + spacing: 4 + clip: true + id: communitiesList + delegate: Item { + // TODO add the serach for the name and category once they exist + visible: !searchBox.text || description.includes(searchBox.text) + height: visible ? communityImage.height + Style.current.smallPadding : 0 + width: parent.width + + RoundedImage { + id: communityImage + width: 40 + height: 40 + // TODO get the real image once it's available + source: "../../../img/ens-header-dark@2x.png" + } + + StyledText { + id: communityName + text: name + anchors.left: communityImage.right + anchors.leftMargin: Style.current.padding + font.pixelSize: 17 + font.weight: Font.Bold + } + + StyledText { + id: communityDesc + text: description + anchors.left: communityName.left + anchors.right: parent.right + anchors.top: communityName.bottom + font.pixelSize: 15 + font.weight: Font.Thin + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + // TODO if already joined, just open the Community in the section + if (joined) { + chatsModel.setActiveCommunity(id) + } else { + chatsModel.setObservedCommunity(id) + openPopup(communityDetailPopup) + } + popup.close() + } + } + } + } + } + + footer: StatusButton { + text: qsTr("Create a community") + anchors.right: parent.right + onClicked: { + openPopup(createCommunitiesPopupComponent) + popup.close() + } + } +} + diff --git a/ui/app/AppLayouts/Chat/components/CommunityDetailPopup.qml b/ui/app/AppLayouts/Chat/components/CommunityDetailPopup.qml new file mode 100644 index 0000000000..704e77cbdd --- /dev/null +++ b/ui/app/AppLayouts/Chat/components/CommunityDetailPopup.qml @@ -0,0 +1,175 @@ +import QtQuick 2.12 +import QtQuick.Dialogs 1.3 +import "../../../../imports" +import "../../../../shared" +import "../../../../shared/status" +import "../ContactsColumn" + +ModalPopup { + property QtObject community: chatsModel.observedCommunity + property string communityId: community.id + property string name: community.name + property string description: community.description + // TODO get the real image once it's available + property string source: "../../../img/ens-header-dark@2x.png" + // TODO set real nb of members + property int nbMembers: 12 + + id: popup + + header: Item { + height: childrenRect.height + width: parent.width + + RoundedImage { + id: communityImg + source: popup.source + width: 40 + height: 40 + } + + StyledTextEdit { + id: communityName + text: popup.name + anchors.top: parent.top + anchors.topMargin: 2 + anchors.left: communityImg.right + anchors.leftMargin: Style.current.smallPadding + font.bold: true + font.pixelSize: 17 + readOnly: true + } + + StyledText { + // TODO get this from access property + text: qsTr("Public community") + anchors.left: communityName.left + anchors.top: communityName.bottom + anchors.topMargin: 2 + font.pixelSize: 15 + font.weight: Font.Thin + color: Style.current.secondaryText + } + } + + StyledText { + id: descriptionText + text: popup.description + wrapMode: Text.WrapAnywhere + width: parent.width + font.pixelSize: 15 + font.weight: Font.Thin + } + + Item { + id: memberContainer + width: parent.width + height: memberImage.height + anchors.top: descriptionText.bottom + anchors.topMargin: Style.current.padding + + SVGImage { + id: memberImage + source: "../../../img/member.svg" + width: 16 + height: 16 + } + + + StyledText { + text: qsTr("%1 members").arg(popup.nbMembers) + wrapMode: Text.WrapAnywhere + width: parent.width + anchors.left: memberImage.right + anchors.leftMargin: 4 + font.pixelSize: 15 + font.weight: Font.Medium + } + } + + + Separator { + id: sep1 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: memberContainer.bottom + anchors.topMargin: Style.current.smallPadding + anchors.leftMargin: -Style.current.padding + anchors.rightMargin: -Style.current.padding + } + + StyledText { + id: chatsTitle + text: qsTr("Chats") + anchors.top: sep1.bottom + anchors.topMargin: Style.current.bigPadding + font.pixelSize: 15 + font.weight: Font.Thin + } + + + ListView { + id: chatsList + width: parent.width + anchors.top: chatsTitle.bottom + anchors.topMargin: 4 + anchors.bottom: parent.bottom + clip: true + model: community.chats + + delegate: Channel { + id: channelItem + unviewedMessagesCount: "" + width: parent.width + name: model.name + lastMessage: model.description + contentType: Constants.messageType + border.width: 0 + color: Style.current.transparent + } + } + + footer: Item { + anchors.fill: parent + + StatusIconButton { + id: backButton + icon.name: "leave_chat" + width: 44 + height: 44 + iconColor: Style.current.primary + highlighted: true + icon.color: Style.current.primary + icon.width: 28 + icon.height: 28 + radius: width / 2 + onClicked: { + openPopup(communitiesPopupComponent) + popup.close() + } + } + + StatusButton { + text: qsTr("Join ‘%1’").arg(popup.name) + anchors.right: parent.right + onClicked: { + const error = chatsModel.joinCommunity(popup.communityId) + + if (error) { + joiningError.text = error + return joiningError.open() + } + + popup.close() + } + } + + MessageDialog { + id: joiningError + title: qsTr("Error joining the community") + icon: StandardIcon.Critical + standardButtons: StandardButton.Ok + } + } +} + diff --git a/ui/app/AppLayouts/Chat/components/CreateCommunityPopup.qml b/ui/app/AppLayouts/Chat/components/CreateCommunityPopup.qml new file mode 100644 index 0000000000..4f1af66bff --- /dev/null +++ b/ui/app/AppLayouts/Chat/components/CreateCommunityPopup.qml @@ -0,0 +1,301 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtGraphicalEffects 1.13 +import QtQuick.Dialogs 1.3 +import "../../../../imports" +import "../../../../shared" +import "../../../../shared/status" + +ModalPopup { + readonly property int maxDescChars: 140 + property string nameValidationError: "" + property string colorValidationError: "" + property string selectedImageValidationError: "" + property string selectedImage: "" + + id: popup + height: 600 + + onOpened: { + nameInput.text = ""; + nameInput.forceActiveFocus(Qt.MouseFocusReason) + } + + function validate() { + nameValidationError = "" + colorValidationError = "" + selectedImageValidationError = "" + + if (nameInput.text === "") { + nameValidationError = qsTr("You need to enter a name") + } else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) { + nameValidationError = qsTr("Please restrict your name to letters, numbers, dashes and spaces") + } else if (nameInput.text.length > 100) { + nameValidationError = qsTr("Your name needs to be 100 characters or shorter") + } + + if (selectedImage === "") { + selectedImageValidationError = qsTr("You need to select an image") + } + + if (colorPicker.text === "") { + colorValidationError = qsTr("You need to enter a color") + } else if (!Utils.isHexColor(colorPicker.text)) { + colorValidationError = qsTr("This field needs to be an hexadecimal color (eg: #4360DF)") + } + + return !nameValidationError && !descriptionTextArea.validationError && !colorValidationError + } + + title: qsTr("New community") + + ScrollView { + property ScrollBar vScrollBar: ScrollBar.vertical + + id: scrollView + anchors.fill: parent + rightPadding: Style.current.padding + anchors.rightMargin: - Style.current.halfPadding + contentHeight: content.height + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + clip: true + + function scrollBackUp() { + vScrollBar.setPosition(0) + } + + Item { + id: content + height: childrenRect.height + width: parent.width + + Input { + id: nameInput + label: qsTr("Name your community") + placeholderText: qsTr("A catchy name") + validationError: popup.nameValidationError + } + + StyledTextArea { + id: descriptionTextArea + label: qsTr("Give it a short description") + placeholderText: qsTr("What your community is about") + validationError: descriptionTextArea.text.length > maxDescChars ? qsTr("The description cannot exceed 140 characters") : "" + anchors.top: nameInput.bottom + anchors.topMargin: Style.current.bigPadding + customHeight: 88 + } + + StyledText { + id: charLimit + text: `${descriptionTextArea.text.length}/${maxDescChars}` + anchors.top: descriptionTextArea.bottom + anchors.topMargin: !descriptionTextArea.validationError ? 5 : - Style.current.smallPadding + anchors.right: descriptionTextArea.right + font.pixelSize: 12 + color: !descriptionTextArea.validationError ? Style.current.textColor : Style.current.danger + } + + StyledText { + id: thumbnailText + text: qsTr("Thumbnail image") + anchors.top: descriptionTextArea.bottom + anchors.topMargin: Style.current.smallPadding + font.pixelSize: 15 + color: Style.current.secondaryText + } + + + Rectangle { + id: addImageButton + color: imagePreview.visible ? "transparent" : Style.current.inputBackground + width: 128 + height: width + radius: width / 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: thumbnailText.bottom + anchors.topMargin: Style.current.padding + + FileDialog { + id: imageDialog + //% "Please choose an image" + title: qsTrId("please-choose-an-image") + folder: shortcuts.pictures + nameFilters: [ + //% "Image files (*.jpg *.jpeg *.png)" + qsTrId("image-files----jpg---jpeg---png-") + ] + onAccepted: { + selectedImage = imageDialog.fileUrls[0] + } + } + + Image { + id: imagePreview + visible: !!popup.selectedImage + source: popup.selectedImage + fillMode: Image.PreserveAspectCrop + width: parent.width + height: parent.height + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + anchors.centerIn: parent + width: imagePreview.width + height: imagePreview.height + radius: imagePreview.width / 2 + } + } + } + + Item { + id: addImageCenter + visible: !imagePreview.visible + width: uploadText.width + height: childrenRect.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + SVGImage { + id: imageImg + source: "../../../img/images_icon.svg" + width: 20 + height: 18 + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + id: uploadText + text: qsTr("Upload") + anchors.top: imageImg.bottom + anchors.topMargin: 5 + font.pixelSize: 15 + color: Style.current.secondaryText + } + } + + Rectangle { + color: Style.current.primary + width: 40 + height: width + radius: width / 2 + anchors.top: parent.top + anchors.right: parent.right + anchors.rightMargin: Style.current.halfPadding + + SVGImage { + source: "../../../img/plusSign.svg" + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: 13 + height: 13 + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: imageDialog.open() + } + } + + Input { + id: colorPicker + label: qsTr("Community color") + placeholderText: qsTr("Pick a color") + anchors.top: addImageButton.bottom + anchors.topMargin: Style.current.smallPadding + validationError: popup.colorValidationError + + StatusIconButton { + icon.name: "caret" + iconRotation: -90 + iconColor: Style.current.textColor + icon.width: 13 + icon.height: 7 + anchors.right: parent.right + anchors.rightMargin: Style.current.smallPadding + anchors.top: parent.top + anchors.topMargin: colorPicker.textField.height / 2 - height / 2 + Style.current.bigPadding + onClicked: colorDialog.open() + } + + ColorDialog { + id: colorDialog + title: qsTr("Please choose a color") + onAccepted: { + colorPicker.text = colorDialog.color + } + } + } + + Separator { + id: separator1 + anchors.top: colorPicker.bottom + anchors.topMargin: Style.current.bigPadding + } + + Item { + id: privateSwitcher + height: privateSwitch.height + width: parent.width + anchors.top: separator1.bottom + anchors.topMargin: Style.current.smallPadding * 2 + + StyledText { + text: qsTr("Private community") + anchors.verticalCenter: parent.verticalCenter + } + + StatusSwitch { + id: privateSwitch + anchors.right: parent.right + } + } + + StyledText { + id: privateExplanation + anchors.top: privateSwitcher.bottom + wrapMode: Text.WordWrap + anchors.topMargin: Style.current.smallPadding * 2 + width: parent.width + text: privateSwitch.checked ? + qsTr("Only members with an invite link will be able to join your community. Private communities are not listed inside Status") : + qsTr("Your community will be public for anyone to join. Public communities are listed inside Status for easy discovery") + } + } + } + + footer: StatusButton { + text: qsTr("Create") + anchors.right: parent.right + onClicked: { + if (!validate()) { + scrollView.scrollBackUp() + return + } + const error = chatsModel.createCommunity(Utils.filterXSS(nameInput.text), + Utils.filterXSS(descriptionTextArea.text), + colorPicker.text, + popup.selectedImage) + + if (error) { + creatingError.text = error + return creatingError.open() + } + + // TODO Open the community once we have designs for it + popup.close() + } + + MessageDialog { + id: creatingError + title: qsTr("Error creating the community") + icon: StandardIcon.Critical + standardButtons: StandardButton.Ok + } + } +} + diff --git a/ui/app/img/communities.svg b/ui/app/img/communities.svg new file mode 100644 index 0000000000..46f5654d50 --- /dev/null +++ b/ui/app/img/communities.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/app/img/member.svg b/ui/app/img/member.svg new file mode 100644 index 0000000000..b5be1d7856 --- /dev/null +++ b/ui/app/img/member.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/imports/Constants.qml b/ui/imports/Constants.qml index 45e68dadf8..4c78febc5a 100644 --- a/ui/imports/Constants.qml +++ b/ui/imports/Constants.qml @@ -7,6 +7,7 @@ QtObject { readonly property int chatTypePublic: 2 readonly property int chatTypePrivateGroupChat: 3 readonly property int chatTypeStatusUpdate: 4 + readonly property int communityPublicChat: 6 readonly property int fetchRangeLast24Hours: 86400 readonly property int fetchRangeLast2Days: 172800 diff --git a/ui/imports/Utils.qml b/ui/imports/Utils.qml index deca2759fa..630197fa96 100644 --- a/ui/imports/Utils.qml +++ b/ui/imports/Utils.qml @@ -183,6 +183,10 @@ QtObject { return (/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/.test(text)) } + function isHexColor(c) { + return (/^#([0-9A-F]{6}|[0-9A-F]{3})$/i.test(c)) + } + function isSpace(c) { return (/( |\t|\n|\r)/.test(c)) } diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index 05eb040e1e..a0c374ac77 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -166,9 +166,17 @@ DISTFILES += \ app/AppLayouts/Chat/ChatColumn/MessageComponents/UserImage.qml \ app/AppLayouts/Chat/ChatColumn/MessageComponents/UsernameLabel.qml \ app/AppLayouts/Chat/ChatColumn/MessageComponents/qmldir \ + app/AppLayouts/Chat/CommunityColumn.qml \ app/AppLayouts/Chat/ContactsColumn/AddChat.qml \ app/AppLayouts/Chat/ContactsColumn/ClosedEmptyView.qml \ app/AppLayouts/Chat/components/ChooseBrowserPopup.qml \ + app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml \ + app/AppLayouts/Chat/ContactsColumn/CommunityComponents/CreateChannelPopup.qml \ + app/AppLayouts/Chat/ContactsColumn/CommunityList.qml \ + app/AppLayouts/Chat/ContactsColumn/CommunityWelcomeBanner.qml \ + app/AppLayouts/Chat/components/CommunitiesPopup.qml \ + app/AppLayouts/Chat/components/CommunityDetailPopup.qml \ + app/AppLayouts/Chat/components/CreateCommunityPopup.qml \ app/AppLayouts/Chat/components/EmojiCategoryButton.qml \ app/AppLayouts/Chat/components/EmojiPopup.qml \ app/AppLayouts/Chat/components/EmojiReaction.qml \ diff --git a/ui/shared/StyledTextArea.qml b/ui/shared/StyledTextArea.qml index 84bbee8494..90154cbf35 100644 --- a/ui/shared/StyledTextArea.qml +++ b/ui/shared/StyledTextArea.qml @@ -50,10 +50,10 @@ Item { id: textArea text: "" font.pixelSize: 15 - wrapMode: Text.WordWrap + wrapMode: Text.WrapAnywhere placeholderText: inputBox.placeholderText - anchors.rightMargin: Style.current.padding - anchors.leftMargin: inputBox.hasIcon ? 36 : Style.current.padding + anchors.rightMargin: Style.current.smallPadding + anchors.leftMargin: inputBox.hasIcon ? 36 : Style.current.smallPadding anchors.bottomMargin: Style.current.smallPadding anchors.topMargin: Style.current.smallPadding anchors.fill: parent diff --git a/ui/shared/status/StatusLetterIdenticon.qml b/ui/shared/status/StatusLetterIdenticon.qml index 38bc4abbd6..9776fa9653 100644 --- a/ui/shared/status/StatusLetterIdenticon.qml +++ b/ui/shared/status/StatusLetterIdenticon.qml @@ -14,7 +14,7 @@ Rectangle { color: { const color = chatsModel.getChannelColor(root.chatName.startsWith("#") ? root.chatName.substr(1) : root.chatName) if (!color) { - return Style.current.transparent + return Style.current.orange } return color } diff --git a/vendor/status-go b/vendor/status-go index 149877a939..7387049d4b 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 149877a939656f3780fb73f5add26e5e205cf26f +Subproject commit 7387049d4b524d3371966fa07ccba3e08b6c652a