From f9817d4f521383a32ba087a4abf1ea0dc43c98db Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Wed, 10 Feb 2021 15:37:17 -0500 Subject: [PATCH] feat: add community requests, permissions, ENS and more --- src/app/chat/event_handling.nim | 2 + src/app/chat/signal_handling.nim | 2 +- src/app/chat/view.nim | 137 ++++++++++---- src/app/chat/views/community_item.nim | 53 +++++- src/app/chat/views/community_list.nim | 45 ++++- .../community_membership_request_list.nim | 91 ++++++++++ src/status/chat.nim | 26 ++- src/status/chat/chat.nim | 35 +++- src/status/contacts.nim | 5 +- src/status/libstatus/chat.nim | 76 ++++++-- src/status/libstatus/contacts.nim | 5 +- src/status/profile/profile.nim | 6 +- src/status/signals/messages.nim | 52 ++++-- src/status/signals/types.nim | 1 + .../Chat/ChatColumn/ChatMessages.qml | 57 ++---- ui/app/AppLayouts/Chat/CommunityColumn.qml | 67 ++++++- .../CommunityComponents/CommunitiesPopup.qml | 30 +-- .../CommunityComponents/CommunityButton.qml | 2 +- .../CommunityDetailPopup.qml | 69 ++++++- .../CommunityComponents/CommunityList.qml | 1 + .../CreateCommunityPopup.qml | 171 +++++++++++------- .../MembershipRadioButton.qml | 49 +++++ .../MembershipRequestsPopup.qml | 165 +++++++++++++++++ .../MembershipRequirementPopup.qml | 101 +++++++++++ .../Sections/ChangeProfilePicModal.qml | 47 +---- ui/app/AppLayouts/Timeline/TimelineLayout.qml | 44 +---- ui/app/img/thumbsDown.svg | 4 + ui/app/img/thumbsUp.svg | 4 + ui/imports/Utils.qml | 18 ++ ui/nim-status-client.pro | 5 + ui/shared/DelegateModelGeneralized.qml | 50 +++++ ui/shared/ImageCropperModal.qml | 54 ++++++ vendor/status-go | 2 +- 33 files changed, 1166 insertions(+), 310 deletions(-) create mode 100644 src/app/chat/views/community_membership_request_list.nim create mode 100644 ui/app/AppLayouts/Chat/CommunityComponents/MembershipRadioButton.qml create mode 100644 ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequestsPopup.qml create mode 100644 ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequirementPopup.qml create mode 100644 ui/app/img/thumbsDown.svg create mode 100644 ui/app/img/thumbsUp.svg create mode 100644 ui/shared/DelegateModelGeneralized.qml create mode 100644 ui/shared/ImageCropperModal.qml diff --git a/src/app/chat/event_handling.nim b/src/app/chat/event_handling.nim index dcfce697b9..eef60474e2 100644 --- a/src/app/chat/event_handling.nim +++ b/src/app/chat/event_handling.nim @@ -28,6 +28,8 @@ proc handleChatEvents(self: ChatController) = if (evArgs.communities.len > 0): for community in evArgs.communities: self.view.addCommunityToList(community) + if (evArgs.communityMembershipRequests.len > 0): + self.view.addMembershipRequests(evArgs.communityMembershipRequests) self.status.events.on("channelUpdate") do(e: Args): var evArgs = ChatUpdateArgs(e) diff --git a/src/app/chat/signal_handling.nim b/src/app/chat/signal_handling.nim index d2ff4d58f1..80281fb48c 100644 --- a/src/app/chat/signal_handling.nim +++ b/src/app/chat/signal_handling.nim @@ -1,7 +1,7 @@ proc handleSignals(self: ChatController) = self.status.events.on(SignalType.Message.event) do(e:Args): var data = MessageSignal(e) - self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities) + self.status.chat.update(data.chats, data.messages, data.emojiReactions, data.communities, data.membershipRequests) self.status.events.on(SignalType.DiscoverySummary.event) do(e:Args): ## Handle mailserver peers being added and removed diff --git a/src/app/chat/view.nim b/src/app/chat/view.nim index fafedf850f..613618d828 100644 --- a/src/app/chat/view.nim +++ b/src/app/chat/view.nim @@ -17,7 +17,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, community_list, community_item] +import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, community_list, community_item, community_membership_request_list] import json_serialization import ../utils/image_utils @@ -48,6 +48,7 @@ QtObject: observedCommunity*: CommunityItemView communityList*: CommunityList joinedCommunityList*: CommunityList + myCommunityRequests*: seq[CommunityMembershipRequest] replyTo: string channelOpenTime*: Table[string, int64] connected: bool @@ -699,6 +700,32 @@ QtObject: QtProperty[QVariant] transactions: read = getTransactions + + proc pendingRequestsToJoinForCommunity*(self: ChatsView, communityId: string): seq[CommunityMembershipRequest] = + result = self.status.chat.pendingRequestsToJoinForCommunity(communityId) + + proc membershipRequestPushed*(self: ChatsView, communityName: string, pubKey: string) {.signal.} + + proc addMembershipRequests*(self: ChatsView, membershipRequests: seq[CommunityMembershipRequest]) = + var communityId: string + var community: Community + for request in membershipRequests: + communityId = request.communityId + community = self.joinedCommunityList.getCommunityById(communityId) + if (community.id == ""): + continue + let alreadyPresentRequestIdx = community.membershipRequests.findIndexById(request.id) + if (alreadyPresentRequestIdx == -1): + community.membershipRequests.add(request) + self.membershipRequestPushed(community.name, request.publicKey) + else: + community.membershipRequests[alreadyPresentRequestIdx] = request + self.joinedCommunityList.replaceCommunity(community) + + # Add to active community list + if (communityId == self.activeCommunity.communityItem.id): + self.activeCommunity.communityMembershipRequestList.addCommunityMembershipRequestItemToList(request) + proc communitiesChanged*(self: ChatsView) {.signal.} proc getCommunitiesIfNotFetched*(self: ChatsView): CommunityList = @@ -723,12 +750,45 @@ QtObject: self.joinedCommunityList.setNewData(communities) self.joinedCommunityList.fetched = true + # Also fetch requests + self.myCommunityRequests = self.status.chat.myPendingRequestsToJoin() + return newQVariant(self.joinedCommunityList) QtProperty[QVariant] joinedCommunities: read = getJoinedComunities notify = joinedCommunitiesChanged + proc activeCommunityChanged*(self: ChatsView) {.signal.} + + proc setActiveCommunity*(self: ChatsView, communityId: string) {.slot.} = + if(communityId == ""): return + self.addMembershipRequests(self.pendingRequestsToJoinForCommunity(communityId)) + 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 joinCommunity*(self: ChatsView, communityId: string, setActive: bool = true): string {.slot.} = + result = "" + try: + self.status.chat.joinCommunity(communityId) + self.joinedCommunityList.addCommunityItemToList(self.communityList.getCommunityById(communityId)) + if (setActive): + self.setActiveCommunity(communityId) + except Exception as e: + error "Error joining the community", msg = e.msg + result = fmt"Error joining the community: {e.msg}" + + proc membershipRequestChanged*(self: ChatsView, communityName: string, accepted: bool) {.signal.} + proc addCommunityToList*(self: ChatsView, community: Community) = let communityCheck = self.communityList.getCommunityById(community.id) if (communityCheck.id == ""): @@ -742,16 +802,27 @@ QtObject: self.joinedCommunityList.addCommunityItemToList(community) else: self.joinedCommunityList.replaceCommunity(community) + elif (community.isMember == true): + discard self.joinCommunity(community.id, false) + var i = 0 + for communityRequest in self.myCommunityRequests: + if (communityRequest.communityId == community.id): + self.membershipRequestChanged(community.name, true) + self.myCommunityRequests.delete(i, i) + break + i = i + 1 - proc createCommunity*(self: ChatsView, name: string, description: string, color: string, imagePath: string): string {.slot.} = + proc isCommunityRequestPending*(self: ChatsView, communityId: string): bool {.slot.} = + for communityRequest in self.myCommunityRequests: + if (communityRequest.communityId == communityId): + return true + return false + + proc createCommunity*(self: ChatsView, name: string, description: string, access: int, ensOnly: bool, imagePath: string, aX: int, aY: int, bX: int, bY: int): string {.slot.} = result = "" try: - # TODO Change this to get it from the user choices - let access = ord(CommunityAccessLevel.public) var image = image_utils.formatImagePath(imagePath) - let tmpImagePath = image_resizer(image, 2000, TMPDIR) - let community = self.status.chat.createCommunity(name, description, color, tmpImagePath, access) - removeFile(tmpImagePath) + let community = self.status.chat.createCommunity(name, description, access, ensOnly, image, aX, aY, bX, bY) if (community.id == ""): return "Community was not created. Please try again later" @@ -777,22 +848,6 @@ QtObject: 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.} = @@ -812,16 +867,6 @@ QtObject: 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: @@ -867,6 +912,32 @@ QtObject: error "Error removing user from the community", msg = e.msg + proc requestToJoinCommunity*(self: ChatsView, communityId: string, ensName: string) {.slot.} = + try: + let requests = self.status.chat.requestToJoinCommunity(communityId, ensName) + for request in requests: + self.myCommunityRequests.add(request) + except Exception as e: + error "Error requesting to join the community", msg = e.msg + + proc acceptRequestToJoinCommunity*(self: ChatsView, requestId: string): string {.slot.} = + try: + self.status.chat.acceptRequestToJoinCommunity(requestId) + self.activeCommunity.communityMembershipRequestList.removeCommunityMembershipRequestItemFromList(requestId) + except Exception as e: + error "Error accepting request to join the community", msg = e.msg + return "Error accepting request to join the community" + return "" + + proc declineRequestToJoinCommunity*(self: ChatsView, requestId: string): string {.slot.} = + try: + self.status.chat.declineRequestToJoinCommunity(requestId) + self.activeCommunity.communityMembershipRequestList.removeCommunityMembershipRequestItemFromList(requestId) + except Exception as e: + error "Error declining request to join the community", msg = e.msg + return "Error declining request to join the community" + return "" + method rowCount*(self: ChatsView, index: QModelIndex = nil): int = result = self.messageList.len diff --git a/src/app/chat/views/community_item.nim b/src/app/chat/views/community_item.nim index 335795d62c..9cc625d8c2 100644 --- a/src/app/chat/views/community_item.nim +++ b/src/app/chat/views/community_item.nim @@ -3,10 +3,12 @@ import ../../../status/[chat/chat, status] import channels_list import ../../../eventemitter import community_members_list +import community_membership_request_list QtObject: type CommunityItemView* = ref object of QObject communityItem*: Community + communityMembershipRequestList*: CommunityMembershipRequestList chats*: ChannelsList members*: CommunityMembersView status*: Status @@ -25,6 +27,7 @@ QtObject: result.status = status result.active = false result.chats = newChannelsList(status) + result.communityMembershipRequestList = newCommunityMembershipRequestList() result.members = newCommunityMembersView(status) result.setup @@ -32,6 +35,7 @@ QtObject: self.communityItem = communityItem self.chats.setChats(communityItem.chats) self.members.setMembers(communityItem.members) + self.communityMembershipRequestList.setNewData(communityItem.membershipRequests) proc activeChanged*(self: CommunityItemView) {.signal.} @@ -88,6 +92,31 @@ QtObject: QtProperty[bool] verified: read = verified + proc ensOnly*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.ensOnly + + QtProperty[bool] ensOnly: + read = ensOnly + + proc canRequestAccess*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.canRequestAccess + + QtProperty[bool] canRequestAccess: + read = canRequestAccess + + proc canManageUsers*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.canManageUsers + + QtProperty[bool] canManageUsers: + read = canManageUsers + + proc canJoin*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.canJoin + + QtProperty[bool] canJoin: + read = canJoin + + proc isMember*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.isMember + + QtProperty[bool] isMember: + read = isMember + proc nbMembers*(self: CommunityItemView): int {.slot.} = result = ?.self.communityItem.members.len QtProperty[int] nbMembers: @@ -104,4 +133,26 @@ QtObject: result = newQVariant(self.members) QtProperty[QVariant] members: - read = getMembers \ No newline at end of file + read = getMembers + + proc getCommunityMembershipRequest*(self: CommunityItemView): QVariant {.slot.} = + result = newQVariant(self.communityMembershipRequestList) + + QtProperty[QVariant] communityMembershipRequests: + read = getCommunityMembershipRequest + + proc thumbnailImage*(self: CommunityItemView): string {.slot.} = + if (self.communityItem.communityImage.isNil): + return "" + result = self.communityItem.communityImage.thumbnail + + QtProperty[string] thumbnailImage: + read = thumbnailImage + + proc largeImage*(self: CommunityItemView): string {.slot.} = + if (self.communityItem.communityImage.isNil): + return "" + result = self.communityItem.communityImage.large + + QtProperty[string] largeImage: + read = largeImage \ No newline at end of file diff --git a/src/app/chat/views/community_list.nim b/src/app/chat/views/community_list.nim index fa46b20b7c..9ef3a35385 100644 --- a/src/app/chat/views/community_list.nim +++ b/src/app/chat/views/community_list.nim @@ -10,12 +10,18 @@ type Id = UserRole + 1, Name = UserRole + 2 Description = UserRole + 3 - # Color = UserRole + 4 - Access = UserRole + 5 - Admin = UserRole + 6 - Joined = UserRole + 7 - Verified = UserRole + 8 - NumMembers = UserRole + 9 + Access = UserRole + 4 + Admin = UserRole + 5 + Joined = UserRole + 6 + Verified = UserRole + 7 + NumMembers = UserRole + 8 + ThumbnailImage = UserRole + 9 + LargeImage = UserRole + 10 + EnsOnly = UserRole + 11 + CanRequestAccess = UserRole + 12 + CanManageUsers = UserRole + 13 + CanJoin = UserRole + 14 + IsMember = UserRole + 15 QtObject: type @@ -50,25 +56,44 @@ QtObject: 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) + of CommunityRoles.EnsOnly: result = newQVariant(communityItem.ensOnly.bool) + of CommunityRoles.CanRequestAccess: result = newQVariant(communityItem.canRequestAccess.bool) + of CommunityRoles.CanManageUsers: result = newQVariant(communityItem.canManageUsers.bool) + of CommunityRoles.CanJoin: result = newQVariant(communityItem.canJoin.bool) + of CommunityRoles.IsMember: result = newQVariant(communityItem.isMember.bool) of CommunityRoles.NumMembers: result = newQVariant(communityItem.members.len) - + of CommunityRoles.ThumbnailImage: + if (not communityItem.communityImage.isNil): + result = newQVariant(communityItem.communityImage.thumbnail) + else: + result = newQVariant("") + of CommunityRoles.LargeImage: + if (not communityItem.communityImage.isNil): + result = newQVariant(communityItem.communityImage.large) + else: + result = newQVariant("") 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", - CommunityRoles.NumMembers.int: "nbMembers" + CommunityRoles.EnsOnly.int: "ensOnly", + CommunityRoles.CanRequestAccess.int: "canRequestAccess", + CommunityRoles.CanManageUsers.int: "canManageUsers", + CommunityRoles.CanJoin.int: "canJoin", + CommunityRoles.IsMember.int: "isMember", + CommunityRoles.NumMembers.int: "nbMembers", + CommunityRoles.ThumbnailImage.int:"thumbnailImage", + CommunityRoles.LargeImage.int:"largeImage" }.toTable proc setNewData*(self: CommunityList, communityList: seq[Community]) = diff --git a/src/app/chat/views/community_membership_request_list.nim b/src/app/chat/views/community_membership_request_list.nim new file mode 100644 index 0000000000..3a88cf1419 --- /dev/null +++ b/src/app/chat/views/community_membership_request_list.nim @@ -0,0 +1,91 @@ +import NimQml, Tables, chronicles +import ../../../status/chat/[chat, message] +import ../../../status/status +import ../../../status/ens +import ../../../status/accounts +import strutils + +type + CommunityMembershipRequestRoles {.pure.} = enum + Id = UserRole + 1, + PublicKey = UserRole + 2 + ChatId = UserRole + 3 + CommunityId = UserRole + 4 + State = UserRole + 5 + Our = UserRole + 6 + +QtObject: + type + CommunityMembershipRequestList* = ref object of QAbstractListModel + communityMembershipRequests*: seq[CommunityMembershipRequest] + + proc setup(self: CommunityMembershipRequestList) = self.QAbstractListModel.setup + + proc delete(self: CommunityMembershipRequestList) = + self.communityMembershipRequests = @[] + self.QAbstractListModel.delete + + proc newCommunityMembershipRequestList*(): CommunityMembershipRequestList = + new(result, delete) + result.communityMembershipRequests = @[] + result.setup() + + method rowCount*(self: CommunityMembershipRequestList, index: QModelIndex = nil): int = self.communityMembershipRequests.len + + method data(self: CommunityMembershipRequestList, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.communityMembershipRequests.len: + return + + let communityMembershipRequestItem = self.communityMembershipRequests[index.row] + let communityMembershipRequestItemRole = role.CommunityMembershipRequestRoles + case communityMembershipRequestItemRole: + of CommunityMembershipRequestRoles.Id: result = newQVariant(communityMembershipRequestItem.id.string) + of CommunityMembershipRequestRoles.PublicKey: result = newQVariant(communityMembershipRequestItem.publicKey.string) + of CommunityMembershipRequestRoles.ChatId: result = newQVariant(communityMembershipRequestItem.chatId.string) + of CommunityMembershipRequestRoles.CommunityId: result = newQVariant(communityMembershipRequestItem.communityId.string) + of CommunityMembershipRequestRoles.State: result = newQVariant(communityMembershipRequestItem.state.int) + of CommunityMembershipRequestRoles.Our: result = newQVariant(communityMembershipRequestItem.our.string) + + method roleNames(self: CommunityMembershipRequestList): Table[int, string] = + { + CommunityMembershipRequestRoles.Id.int: "id", + CommunityMembershipRequestRoles.PublicKey.int: "publicKey", + CommunityMembershipRequestRoles.ChatId.int: "chatId", + CommunityMembershipRequestRoles.CommunityId.int: "communityId", + CommunityMembershipRequestRoles.State.int: "state", + CommunityMembershipRequestRoles.Our.int: "our" + }.toTable + + proc nbRequestsChanged*(self: CommunityMembershipRequestList) {.signal.} + + proc nbRequests*(self: CommunityMembershipRequestList): int {.slot.} = result = self.communityMembershipRequests.len + + QtProperty[int] nbRequests: + read = nbRequests + notify = nbRequestsChanged + + proc setNewData*(self: CommunityMembershipRequestList, communityMembershipRequestList: seq[CommunityMembershipRequest]) = + self.beginResetModel() + self.communityMembershipRequests = communityMembershipRequestList + self.endResetModel() + self.nbRequestsChanged() + + proc addCommunityMembershipRequestItemToList*(self: CommunityMembershipRequestList, communityMemberphipRequest: CommunityMembershipRequest) = + self.beginInsertRows(newQModelIndex(), self.communityMembershipRequests.len, self.communityMembershipRequests.len) + self.communityMembershipRequests.add(communityMemberphipRequest) + self.endInsertRows() + self.nbRequestsChanged() + + proc removeCommunityMembershipRequestItemFromList*(self: CommunityMembershipRequestList, id: string) = + let idx = self.communityMembershipRequests.findIndexById(id) + self.beginRemoveRows(newQModelIndex(), idx, idx) + self.communityMembershipRequests.delete(idx) + self.endRemoveRows() + self.nbRequestsChanged() + + proc getCommunityMembershipRequestById*(self: CommunityMembershipRequestList, communityMembershipRequestId: string): CommunityMembershipRequest = + for communityMembershipRequest in self.communityMembershipRequests: + if communityMembershipRequest.id == communityMembershipRequestId: + return communityMembershipRequest diff --git a/src/status/chat.nim b/src/status/chat.nim index 636fbbd01a..ee4bdcd0ad 100644 --- a/src/status/chat.nim +++ b/src/status/chat.nim @@ -25,6 +25,7 @@ type contacts*: seq[Profile] emojiReactions*: seq[Reaction] communities*: seq[Community] + communityMembershipRequests*: seq[CommunityMembershipRequest] ChatIdArg* = ref object of Args chatId*: string @@ -70,7 +71,7 @@ proc newChatModel*(events: EventEmitter): ChatModel = proc delete*(self: ChatModel) = discard -proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction], communities: seq[Community]) = +proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiReactions: seq[Reaction], communities: seq[Community], communityMembershipRequests: seq[CommunityMembershipRequest]) = for chat in chats: if chat.isActive: self.channels[chat.id] = chat @@ -84,7 +85,7 @@ proc update*(self: ChatModel, chats: seq[Chat], messages: seq[Message], emojiRea if self.lastMessageTimestamps[chatId] > ts: self.lastMessageTimestamps[chatId] = ts - self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[], emojiReactions: emojiReactions, communities: communities)) + self.events.emit("chatUpdate", ChatUpdateArgs(messages: messages, chats: chats, contacts: @[], emojiReactions: emojiReactions, communities: communities, communityMembershipRequests: communityMembershipRequests)) proc hasChannel*(self: ChatModel, chatId: string): bool = self.channels.hasKey(chatId) @@ -412,8 +413,8 @@ proc getAllComunities*(self: ChatModel): seq[Community] = 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 createCommunity*(self: ChatModel, name: string, description: string, access: int, ensOnly: bool, imageUrl: string, aX: int, aY: int, bX: int, bY: int): Community = + result = status_chat.createCommunity(name, description, access, ensOnly, imageUrl, aX, aY, bX, bY) proc createCommunityChannel*(self: ChatModel, communityId: string, name: string, description: string): Chat = result = status_chat.createCommunityChannel(communityId, name, description) @@ -436,4 +437,19 @@ proc exportCommunity*(self: ChatModel, communityId: string): string = result = status_chat.exportCommunity(communityId) proc importCommunity*(self: ChatModel, communityKey: string) = - status_chat.importCommunity(communityKey) \ No newline at end of file + status_chat.importCommunity(communityKey) + +proc requestToJoinCommunity*(self: ChatModel, communityKey: string, ensName: string): seq[CommunityMembershipRequest] = + status_chat.requestToJoinCommunity(communityKey, ensName) + +proc acceptRequestToJoinCommunity*(self: ChatModel, requestId: string) = + status_chat.acceptRequestToJoinCommunity(requestId) + +proc declineRequestToJoinCommunity*(self: ChatModel, requestId: string) = + status_chat.declineRequestToJoinCommunity(requestId) + +proc pendingRequestsToJoinForCommunity*(self: ChatModel, communityKey: string): seq[CommunityMembershipRequest] = + result = status_chat.pendingRequestsToJoinForCommunity(communityKey) + +proc myPendingRequestsToJoin*(self: ChatModel): seq[CommunityMembershipRequest] = + result = status_chat.myPendingRequestsToJoin() \ No newline at end of file diff --git a/src/status/chat/chat.nim b/src/status/chat/chat.nim index b0d8aa157d..074018f5fc 100644 --- a/src/status/chat/chat.nim +++ b/src/status/chat/chat.nim @@ -1,5 +1,6 @@ import strformat, json, sequtils from message import Message +import ../libstatus/types type ChatType* {.pure.}= enum Unknown = 0, @@ -74,13 +75,22 @@ type Chat* = ref object membershipUpdateEvents*: seq[ChatMembershipEvent] hasMentions*: bool muted*: bool + canPost*: bool ensName*: string type CommunityAccessLevel* = enum - unknown = 0 - public = 1 - invitationOnly = 2 - onRequest = 3 + unknown = 0 + public = 1 + invitationOnly = 2 + onRequest = 3 + +type CommunityMembershipRequest* = object + id*: string + publicKey*: string + chatId*: string + communityId*: string + state*: int + our*: string type Community* = object id*: string @@ -88,11 +98,17 @@ type Community* = object description*: string chats*: seq[Chat] members*: seq[string] - # color*: string access*: int admin*: bool joined*: bool verified*: bool + ensOnly*: bool + canRequestAccess*: bool + canManageUsers*: bool + canJoin*: bool + isMember*: bool + communityImage*: IdentityImage + membershipRequests*: seq[CommunityMembershipRequest] proc `$`*(self: Chat): string = result = fmt"Chat(id:{self.id}, name:{self.name}, active:{self.isActive}, type:{self.chatType})" @@ -134,6 +150,15 @@ proc findIndexById*(self: seq[Community], id: string): int = result = idx break +proc findIndexById*(self: seq[CommunityMembershipRequest], 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/contacts.nim b/src/status/contacts.nim index 8b7d00bf58..2c7a5d3cec 100644 --- a/src/status/contacts.nim +++ b/src/status/contacts.nim @@ -41,6 +41,7 @@ proc unblockContact*(self: ContactModel, id: string): string = var contact = self.getContactByID(id) contact.systemTags.delete(contact.systemTags.find(":contact/blocked")) discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.ensVerifiedAt, contact.ensVerificationRetries,contact.alias, contact.identicon, contact.identityImage.thumbnail, contact.systemTags, contact.localNickname) + discard status_contacts.saveContact(contact.id, contact.ensVerified, contact.ensName, contact.alias, contact.identicon, contact.systemTags, contact.localNickname) self.events.emit("contactUnblocked", Args()) proc getAllContacts*(): seq[Profile] = @@ -64,9 +65,7 @@ proc addContact*(self: ContactModel, id: string, localNickname: string): string alias: alias, ensName: "", ensVerified: false, - ensVerifiedAt: 0, appearance: 0, - ensVerificationRetries: 0, systemTags: @[] ) @@ -98,9 +97,7 @@ proc addContact*(self: ContactModel, id: string, localNickname: string): string alias: contact.alias, ensName: contact.ensName, ensVerified: contact.ensVerified, - ensVerifiedAt: contact.ensVerifiedAt, appearance: 0, - ensVerificationRetries: contact.ensVerificationRetries, systemTags: contact.systemTags, localNickname: nickname ) diff --git a/src/status/libstatus/chat.nim b/src/status/libstatus/chat.nim index 717787fa0f..f7bdc94b4b 100644 --- a/src/status/libstatus/chat.nim +++ b/src/status/libstatus/chat.nim @@ -257,24 +257,18 @@ proc getJoinedComunities*(): seq[Community] = communities.add(community) return communities -proc createCommunity*(name: string, description: string, color: string, image: string, access: int): Community = +proc createCommunity*(name: string, description: string, access: int, ensOnly: bool, imageUrl: string, aX: int, aY: int, bX: int, bY: 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 - # } - # ] - } + # TODO this will need to be renamed membership (small m) + "Membership": access, + "name": name, + "description": description, + "ensOnly": ensOnly, + "image": imageUrl, + "imageAx": aX, + "imageAy": aY, + "imageBx": bX, + "imageBy": bY }]).parseJSON() if rpcResult{"result"}.kind != JNull: @@ -321,4 +315,50 @@ proc importCommunity*(communityKey: string) = discard callPrivateRPC("importCommunity".prefix, %*[communityKey]) proc removeUserFromCommunity*(communityId: string, pubKey: string) = - discard callPrivateRPC("removeUserFromCommunity".prefix, %*[communityId, pubKey]) \ No newline at end of file + discard callPrivateRPC("removeUserFromCommunity".prefix, %*[communityId, pubKey]) + +proc requestToJoinCommunity*(communityId: string, ensName: string): seq[CommunityMembershipRequest] = + let rpcResult = callPrivateRPC("requestToJoinCommunity".prefix, %*[{ + "communityId": communityId, + "ensName": ensName + }]).parseJSON() + + var communityRequests: seq[CommunityMembershipRequest] = @[] + + if rpcResult{"result"}{"requestsToJoinCommunity"}.kind != JNull: + for jsonCommunityReqest in rpcResult["result"]["requestsToJoinCommunity"]: + communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest()) + + return communityRequests + +proc acceptRequestToJoinCommunity*(requestId: string) = + discard callPrivateRPC("acceptRequestToJoinCommunity".prefix, %*[{ + "id": requestId + }]) + +proc declineRequestToJoinCommunity*(requestId: string) = + discard callPrivateRPC("declineRequestToJoinCommunity".prefix, %*[{ + "id": requestId + }]) + +proc pendingRequestsToJoinForCommunity*(communityId: string): seq[CommunityMembershipRequest] = + let rpcResult = callPrivateRPC("pendingRequestsToJoinForCommunity".prefix, %*[communityId]).parseJSON() + + var communityRequests: seq[CommunityMembershipRequest] = @[] + + if rpcResult{"result"}.kind != JNull: + for jsonCommunityReqest in rpcResult["result"]: + communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest()) + + return communityRequests + +proc myPendingRequestsToJoin*(): seq[CommunityMembershipRequest] = + let rpcResult = callPrivateRPC("myPendingRequestsToJoin".prefix).parseJSON() + + var communityRequests: seq[CommunityMembershipRequest] = @[] + + if rpcResult{"result"}.kind != JNull: + for jsonCommunityReqest in rpcResult["result"]: + communityRequests.add(jsonCommunityReqest.toCommunityMembershipRequest()) + + return communityRequests \ No newline at end of file diff --git a/src/status/libstatus/contacts.nim b/src/status/libstatus/contacts.nim index f6ed9666ea..5086d3d657 100644 --- a/src/status/libstatus/contacts.nim +++ b/src/status/libstatus/contacts.nim @@ -8,8 +8,6 @@ proc blockContact*(contact: Profile): string = { "id": contact.id, "ensVerified": contact.ensVerified, - "ensVerifiedAt": contact.ensVerifiedAt, - "ensVerificationRetries": contact.ensVerificationRetries, "alias": contact.alias, "identicon": contact.identicon, "systemTags": contact.systemTags @@ -27,12 +25,11 @@ proc getContacts*(): JsonNode = return response["result"] proc saveContact*(id: string, ensVerified: bool, ensName: string, ensVerifiedAt: int, ensVerificationRetries: int, alias: string, identicon: string, thumbnail: string, systemTags: seq[string], localNickname: string): string = +proc saveContact*(id: string, ensVerified: bool, ensName: string, alias: string, identicon: string, systemTags: seq[string], localNickname: string): string = let payload = %* [{ "id": id, "name": ensName, "ensVerified": ensVerified, - "ensVerifiedAt": ensVerifiedAt, - "ensVerificationRetries": ensVerificationRetries, "alias": alias, "identicon": identicon, "images": {"thumbnail": {"Payload": thumbnail.partition(",")[2]}}, diff --git a/src/status/profile/profile.nim b/src/status/profile/profile.nim index 0b75d49b38..bac361df1c 100644 --- a/src/status/profile/profile.nim +++ b/src/status/profile/profile.nim @@ -5,7 +5,7 @@ type Profile* = ref object id*, alias*, username*, identicon*, address*, ensName*, localNickname*: string ensVerified*: bool identityImage*: IdentityImage - ensVerifiedAt*, ensVerificationRetries*, appearance*: int + appearance*: int systemTags*: seq[string] proc isContact*(self: Profile): bool = @@ -22,9 +22,7 @@ proc toProfileModel*(account: Account): Profile = alias: account.name, ensName: "", ensVerified: false, - ensVerifiedAt: 0, appearance: 0, - ensVerificationRetries: 0, systemTags: @[] ) @@ -43,8 +41,6 @@ proc toProfileModel*(profile: JsonNode): Profile = ensName: "", ensVerified: profile["ensVerified"].getBool, appearance: 0, - ensVerifiedAt: profile["ensVerifiedAt"].getInt, - ensVerificationRetries: profile["ensVerificationRetries"].getInt, systemTags: systemTags ) diff --git a/src/status/signals/messages.nim b/src/status/signals/messages.nim index c261b0a4c4..8f1c0f6dad 100644 --- a/src/status/signals/messages.nim +++ b/src/status/signals/messages.nim @@ -21,6 +21,8 @@ proc toReaction*(jsonReaction: JsonNode): Reaction proc toCommunity*(jsonCommunity: JsonNode): Community +proc toCommunityMembershipRequest*(jsonCommunityMembershipRequest: JsonNode): CommunityMembershipRequest + proc fromEvent*(event: JsonNode): Signal = var signal:MessageSignal = MessageSignal() signal.messages = @[] @@ -61,6 +63,11 @@ proc fromEvent*(event: JsonNode): Signal = for jsonCommunity in event["event"]["communities"]: signal.communities.add(jsonCommunity.toCommunity) + if event["event"]{"requestsToJoinCommunity"} != nil: + debug "requests", event = event["event"]["requestsToJoinCommunity"] + for jsonCommunity in event["event"]["requestsToJoinCommunity"]: + signal.membershipRequests.add(jsonCommunity.toCommunityMembershipRequest) + result = signal proc toChatMember*(jsonMember: JsonNode): ChatMember = @@ -164,32 +171,53 @@ proc toChat*(jsonChat: JsonNode): Chat = 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, + name: jsonCommunity{"name"}.getStr, + description: jsonCommunity{"description"}.getStr, + access: jsonCommunity{"permissions"}{"access"}.getInt, admin: jsonCommunity{"admin"}.getBool, joined: jsonCommunity{"joined"}.getBool, verified: jsonCommunity{"verified"}.getBool, + ensOnly: jsonCommunity{"permissions"}{"ens_only"}.getBool, + canRequestAccess: jsonCommunity{"canRequestAccess"}.getBool, + canManageUsers: jsonCommunity{"canManageUsers"}.getBool, + canJoin: jsonCommunity{"canJoin"}.getBool, + isMember: jsonCommunity{"isMember"}.getBool, chats: newSeq[Chat](), - members: newSeq[string]() + members: newSeq[string](), + communityImage: IdentityImage() ) - if jsonCommunity["description"].hasKey("chats") and jsonCommunity["description"]["chats"].kind != JNull: - for chatId, chat in jsonCommunity{"description"}{"chats"}: + if jsonCommunity.hasKey("images") and jsonCommunity["images"].kind != JNull: + if jsonCommunity["images"].hasKey("thumbnail"): + result.communityImage.thumbnail = jsonCommunity["images"]["thumbnail"]["uri"].str + if jsonCommunity["images"].hasKey("large"): + result.communityImage.large = jsonCommunity["images"]["large"]["uri"].str + + if jsonCommunity.hasKey("chats") and jsonCommunity["chats"].kind != JNull: + for chatId, chat in jsonCommunity{"chats"}: result.chats.add(Chat( id: result.id & chatId, - name: chat{"identity"}{"display_name"}.getStr, - description: chat{"identity"}{"description"}.getStr, + name: chat{"name"}.getStr, + canPost: chat{"canPost"}.getBool, # TODO get this from access - chatType: ChatType.Public#chat{"permissions"}{"access"}.getInt, + chatType: ChatType.Public#chat{"permissions"}{"access"}.getInt, )) - if jsonCommunity["description"].hasKey("members") and jsonCommunity["description"]["members"].kind != JNull: + if jsonCommunity.hasKey("members") and jsonCommunity["members"].kind != JNull: # memberInfo is empty for now - for memberPubKey, memeberInfo in jsonCommunity{"description"}{"members"}: + for memberPubKey, memeberInfo in jsonCommunity{"members"}: result.members.add(memberPubKey) +proc toCommunityMembershipRequest*(jsonCommunityMembershipRequest: JsonNode): CommunityMembershipRequest = + result = CommunityMembershipRequest( + id: jsonCommunityMembershipRequest{"id"}.getStr, + publicKey: jsonCommunityMembershipRequest{"publicKey"}.getStr, + chatId: jsonCommunityMembershipRequest{"chatId"}.getStr, + state: jsonCommunityMembershipRequest{"state"}.getInt, + communityId: jsonCommunityMembershipRequest{"communityId"}.getStr, + our: jsonCommunityMembershipRequest{"our"}.getStr, + ) + proc toTextItem*(jsonText: JsonNode): TextItem = result = TextItem( literal: jsonText{"literal"}.getStr, diff --git a/src/status/signals/types.nim b/src/status/signals/types.nim index 3e1e5c67f7..d5a7c16595 100644 --- a/src/status/signals/types.nim +++ b/src/status/signals/types.nim @@ -34,6 +34,7 @@ type MessageSignal* = ref object of Signal installations*: seq[Installation] emojiReactions*: seq[Reaction] communities*: seq[Community] + membershipRequests*: seq[CommunityMembershipRequest] type MailserverRequestCompletedSignal* = ref object of Signal requestID*: string diff --git a/ui/app/AppLayouts/Chat/ChatColumn/ChatMessages.qml b/ui/app/AppLayouts/Chat/ChatColumn/ChatMessages.qml index acb4d116d7..b52b1a5e0e 100644 --- a/ui/app/AppLayouts/Chat/ChatColumn/ChatMessages.qml +++ b/ui/app/AppLayouts/Chat/ChatColumn/ChatMessages.qml @@ -201,6 +201,21 @@ ScrollView { } } } + + onMembershipRequestChanged: function (communityName, accepted) { + systemTray.showMessage("Status", + accepted ? qsTr("You have been accepted into the ‘%1’ community").arg(communityName) : + qsTr("Your request to join the ‘%1’ community was declined").arg(communityName), + SystemTrayIcon.NoIcon, + Constants.notificationPopupTTL) + } + + onMembershipRequestPushed: function (communityName, pubKey) { + systemTray.showMessage(qsTr("New membership request"), + qsTr("%1 asks to join ‘%2’").arg(Utils.getDisplayName(pubKey)).arg(communityName), + SystemTrayIcon.NoIcon, + Constants.notificationPopupTTL) + } } Connections { @@ -237,52 +252,12 @@ ScrollView { icon: StandardIcon.Critical } - DelegateModel { + DelegateModelGeneralized { id: messageListDelegate property var lessThan: [ function(left, right) { return left.clock > right.clock } ] - property int sortOrder: 0 - onSortOrderChanged: items.setGroups(0, items.count, "unsorted") - - function insertPosition(lessThan, item) { - var lower = 0 - var upper = items.count - while (lower < upper) { - var middle = Math.floor(lower + (upper - lower) / 2) - var result = lessThan(item.model, items.get(middle).model); - if (result) { - upper = middle - } else { - lower = middle + 1 - } - } - return lower - } - - function sort(lessThan) { - while (unsortedItems.count > 0) { - var item = unsortedItems.get(0) - var index = insertPosition(lessThan, 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.lessThan.length) - setGroups(0, count, "items") - else { - messageListDelegate.sort(messageListDelegate.lessThan[messageListDelegate.sortOrder]) - } - } - } model: messageList delegate: Message { diff --git a/ui/app/AppLayouts/Chat/CommunityColumn.qml b/ui/app/AppLayouts/Chat/CommunityColumn.qml index 101e25f3cc..9459c80845 100644 --- a/ui/app/AppLayouts/Chat/CommunityColumn.qml +++ b/ui/app/AppLayouts/Chat/CommunityColumn.qml @@ -1,5 +1,6 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 +import QtGraphicalEffects 1.13 import QtQuick.Layouts 1.13 import "../../../imports" @@ -86,10 +87,74 @@ Item { } } + Rectangle { + property int nbRequests: chatsModel.activeCommunity.communityMembershipRequests.nbRequests + + id: membershipRequestsBtn + visible: nbRequests > 0 + width: parent.width + height: visible ? 52 : 0 + color: Style.current.secondaryBackground + anchors.top: communityHeader.bottom + anchors.topMargin: visible ? Style.current.halfPadding : 0 + + StyledText { + text: qsTr("Membership requests") + font.pixelSize: 15 + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: badge + anchors.right: caret.left + anchors.rightMargin: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + color: Style.current.blue + width: 22 + height: 22 + radius: width / 2 + Text { + font.pixelSize: 12 + color: Style.current.white + anchors.centerIn: parent + text: membershipRequestsBtn.nbRequests.toString() + } + } + + SVGImage { + id: caret + source: "../../img/caret.svg" + fillMode: Image.PreserveAspectFit + rotation: -90 + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + width: 13 + + + ColorOverlay { + anchors.fill: parent + source: parent + color: Style.current.darkGrey + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: membershipRequestPopup.open() + } + } + + MembershipRequestsPopup { + id: membershipRequestPopup + } ScrollView { id: chatGroupsContainer - anchors.top: communityHeader.bottom + anchors.top: membershipRequestsBtn.bottom anchors.topMargin: Style.current.padding anchors.bottom: parent.bottom anchors.left: parent.left diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/CommunitiesPopup.qml b/ui/app/AppLayouts/Chat/CommunityComponents/CommunitiesPopup.qml index 5c42708415..5e2c1f0fc1 100644 --- a/ui/app/AppLayouts/Chat/CommunityComponents/CommunitiesPopup.qml +++ b/ui/app/AppLayouts/Chat/CommunityComponents/CommunitiesPopup.qml @@ -39,7 +39,7 @@ ModalPopup { ListView { anchors.fill: parent - model: chatsModel.communities + model: communitiesDelegateModel spacing: 4 clip: true id: communitiesList @@ -50,7 +50,7 @@ ModalPopup { width: parent.width height: childrenRect.height + Style.current.halfPadding StyledText { - text: section + text: section.toUpperCase() } Separator { anchors.left: popup.left @@ -58,8 +58,19 @@ ModalPopup { } } + } + + DelegateModelGeneralized { + id: communitiesDelegateModel + lessThan: [ + function(left, right) { + return left.name.toLowerCase() < right.name.toLowerCase() + } + ] + + model: chatsModel.communities delegate: Item { - // TODO add the serach for the name and category once they exist + // TODO add the search for the name and category once they exist visible: { if (!searchBox.text) { return true @@ -74,8 +85,7 @@ ModalPopup { id: communityImage width: 40 height: 40 - // TODO get the real image once it's available - source: "../../../img/ens-header-dark@2x.png" + source: thumbnailImage } StyledText { @@ -100,11 +110,9 @@ ModalPopup { StyledText { id: communityMembers - text: nbMembers === 1 ? - //% "1 member" - qsTrId("1-member") : - //% "%1 members" - qsTrId("-1-members").arg(nbMembers) + text: nbMembers === 1 ? + qsTr("1 member") : + qsTr("%1 members").arg(nbMembers) anchors.left: communityDesc.left anchors.right: parent.right anchors.top: communityDesc.bottom @@ -117,7 +125,7 @@ ModalPopup { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - if (joined) { + if (joined && isMember) { chatsModel.setActiveCommunity(id) } else { chatsModel.setObservedCommunity(id) diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/CommunityButton.qml b/ui/app/AppLayouts/Chat/CommunityComponents/CommunityButton.qml index 254248a0d9..03716ae2d8 100644 --- a/ui/app/AppLayouts/Chat/CommunityComponents/CommunityButton.qml +++ b/ui/app/AppLayouts/Chat/CommunityComponents/CommunityButton.qml @@ -10,7 +10,7 @@ Rectangle { property string name: "channelName" property string description: "channel description" property string unviewedMessagesCount: "0" - property string image: "../../../img/ens-header-dark@2x.png" + property string image property bool hasMentions: false property string searchStr: "" property bool isCompact: appSettings.useCompactMode diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/CommunityDetailPopup.qml b/ui/app/AppLayouts/Chat/CommunityComponents/CommunityDetailPopup.qml index 7300f185ff..030d2c2a80 100644 --- a/ui/app/AppLayouts/Chat/CommunityComponents/CommunityDetailPopup.qml +++ b/ui/app/AppLayouts/Chat/CommunityComponents/CommunityDetailPopup.qml @@ -11,9 +11,11 @@ ModalPopup { property string name: community.name property string description: community.description property int access: community.access - // TODO get the real image once it's available - property string source: "../../../img/ens-header-dark@2x.png" + property string source: community.thumbnailImage property int nbMembers: community.nbMembers + property bool ensOnly: community.ensOnly + property bool canJoin: community.canJoin + property bool canRequestAccess: community.canRequestAccess id: popup @@ -41,7 +43,7 @@ ModalPopup { } StyledText { - // TODO get this from access property + id: accessText text: { switch(access) { //% "Public community" @@ -61,6 +63,17 @@ ModalPopup { font.weight: Font.Thin color: Style.current.secondaryText } + + StyledText { + visible: popup.ensOnly + text: qsTr(" - ENS Only") + anchors.left: accessText.right + anchors.verticalCenter: accessText.verticalCenter + anchors.topMargin: 2 + font.pixelSize: 15 + font.weight: Font.Thin + color: Style.current.secondaryText + } } StyledText { @@ -168,11 +181,55 @@ ModalPopup { } StatusButton { - //% "Join ‘%1’" - text: qsTrId("join---1-").arg(popup.name) + property bool isPendingRequest: { + if (access !== Constants.communityChatOnRequestAccess) { + return false + } + return chatsModel.isCommunityRequestPending(communityId) + } + text: { + if (ensOnly && !profileModel.profile.ensVerified) { + return qsTr("Membership requires an ENS username") + } + if (canJoin) { + return qsTr("Join ‘%1’").arg(popup.name); + } + if (isPendingRequest) { + return qsTr("Pending") + } + switch(access) { + case Constants.communityChatPublicAccess: return qsTr("Join ‘%1’").arg(popup.name); + case Constants.communityChatInvitationOnlyAccess: return qsTr("You need to be invited"); + case Constants.communityChatOnRequestAccess: return qsTr("Request to join ‘%1’").arg(popup.name); + default: return qsTr("Unknown community"); + } + } + enabled: { + if (ensOnly && !profileModel.profile.ensVerified) { + return false + } + if (canJoin) { + return true + } + if (access === Constants.communityChatInvitationOnlyAccess || isPendingRequest) { + return false + } + return true + } + anchors.right: parent.right onClicked: { - const error = chatsModel.joinCommunity(popup.communityId) + let error + if (access === Constants.communityChatOnRequestAccess) { + error = chatsModel.requestToJoinCommunity(popup.communityId, + profileModel.profile.ensVerified ? profileModel.profile.username : "") + if (!error) { + enabled = false + text = qsTr("Pending") + } + } else { + error = chatsModel.joinCommunity(popup.communityId) + } if (error) { joiningError.text = error diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/CommunityList.qml b/ui/app/AppLayouts/Chat/CommunityComponents/CommunityList.qml index 5978f3063b..d1ffae6ecc 100644 --- a/ui/app/AppLayouts/Chat/CommunityComponents/CommunityList.qml +++ b/ui/app/AppLayouts/Chat/CommunityComponents/CommunityList.qml @@ -25,6 +25,7 @@ Item { name: model.name description: model.description searchStr: root.searchStr + image: model.thumbnailImage } } diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/CreateCommunityPopup.qml b/ui/app/AppLayouts/Chat/CommunityComponents/CreateCommunityPopup.qml index 854e259516..78e8f78328 100644 --- a/ui/app/AppLayouts/Chat/CommunityComponents/CreateCommunityPopup.qml +++ b/ui/app/AppLayouts/Chat/CommunityComponents/CreateCommunityPopup.qml @@ -12,6 +12,13 @@ ModalPopup { property string colorValidationError: "" property string selectedImageValidationError: "" property string selectedImage: "" + property var imageDimensions: ({ + aX: 0, + aY: 0, + bY: 1, + bY: 1 + }) + property QtObject community: chatsModel.activeCommunity property bool isEdit: false @@ -51,13 +58,11 @@ ModalPopup { selectedImageValidationError = qsTrId("you-need-to-select-an-image") } - if (colorPicker.text === "") { - //% "You need to enter a color" - colorValidationError = qsTrId("you-need-to-enter-a-color") - } else if (!Utils.isHexColor(colorPicker.text)) { - //% "This field needs to be an hexadecimal color (eg: #4360DF)" - colorValidationError = qsTrId("this-field-needs-to-be-an-hexadecimal-color--eg---4360df-") - } +// 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 } @@ -73,8 +78,10 @@ ModalPopup { id: scrollView anchors.fill: parent - rightPadding: Style.current.padding - anchors.rightMargin: - Style.current.halfPadding + rightPadding: Style.current.bigPadding + anchors.rightMargin: - Style.current.bigPadding + leftPadding: Style.current.bigPadding + anchors.leftMargin: - Style.current.bigPadding contentHeight: content.height ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AlwaysOn @@ -154,7 +161,8 @@ ModalPopup { qsTrId("image-files----jpg---jpeg---png-") ] onAccepted: { - selectedImage = imageDialog.fileUrls[0] + popup.selectedImage = imageDialog.fileUrls[0] + imageCropperModal.open() } } @@ -227,82 +235,108 @@ ModalPopup { cursorShape: Qt.PointingHandCursor onClicked: imageDialog.open() } - } - Input { - id: colorPicker - //% "Community color" - label: qsTrId("community-color") - //% "Pick a color" - placeholderText: qsTrId("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 - //% "Please choose a color" - title: qsTrId("please-choose-a-color") - onAccepted: { - colorPicker.text = colorDialog.color + ImageCropperModal { + id: imageCropperModal + selectedImage: popup.selectedImage + onCropFinished: { + imageDimensions.aX = aX + imageDimensions.aY = aY + imageDimensions.bX = bX + imageDimensions.bY = bY } } } + // TODO re-add color picker when status-go supports it +// 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.top: addImageButton.bottom anchors.topMargin: isEdit ? 0 : Style.current.bigPadding visible: !isEdit } - Item { - visible: !isEdit - id: privateSwitcher - height: visible ? privateSwitch.height : 0 - width: parent.width + StatusSettingsLineButton { + id: membershipRequirementSetting anchors.top: separator1.bottom - anchors.topMargin: isEdit ? 0 : Style.current.smallPadding * 2 - - StyledText { - //% "Private community" - text: qsTrId("private-community") - anchors.verticalCenter: parent.verticalCenter + anchors.topMargin: Style.current.halfPadding + text: qsTr("Membership requirement") + currentValue: { + switch (membershipRequirementSettingPopup.checkedMembership) { + case Constants.communityChatInvitationOnlyAccess: return qsTr("Require invite from another member") + case Constants.communityChatOnRequestAccess: return qsTr("Require approval") + default: return qsTr("No requirement") + } } - - StatusSwitch { - id: privateSwitch - anchors.right: parent.right + onClicked: { + membershipRequirementSettingPopup.open() } } StyledText { visible: !isEdit - height: visible ? 50 : 0 + height: visible ? implicitHeight : 0 id: privateExplanation - anchors.top: privateSwitcher.bottom + anchors.top: membershipRequirementSetting.bottom wrapMode: Text.WordWrap - anchors.topMargin: isEdit ? 0 : Style.current.smallPadding * 2 + anchors.topMargin: isEdit ? 0 : Style.current.halfPadding width: parent.width - text: privateSwitch.checked ? - //% "Only members with an invite link will be able to join your community. Private communities are not listed inside Status" - qsTrId("only-members-with-an-invite-link-will-be-able-to-join-your-community--private-communities-are-not-listed-inside-status") : - //% "Your community will be public for anyone to join. Public communities are listed inside Status for easy discovery" - qsTrId("your-community-will-be-public-for-anyone-to-join--public-communities-are-listed-inside-status-for-easy-discovery") + text: qsTr("You can require new members to meet certain criteria before they can join. This can be changed at any time") } + + StatusSettingsLineButton { + id: ensOnlySwitch + anchors.top: privateExplanation.bottom + anchors.topMargin: Style.current.padding + text: qsTr("Require ENS username") + isSwitch: true + onClicked: switchChecked = checked + } + + StyledText { + visible: !isEdit + height: visible ? implicitHeight : 0 + id: ensExplanation + anchors.top: ensOnlySwitch.bottom + wrapMode: Text.WordWrap + anchors.topMargin: isEdit ? 0 : Style.current.halfPadding + width: parent.width + text: qsTr("Your community requires an ENS username to be able to join") + } + } + + MembershipRequirementPopup { + id: membershipRequirementSettingPopup } } @@ -325,8 +359,13 @@ ModalPopup { } else { error = chatsModel.createCommunity(Utils.filterXSS(nameInput.text), Utils.filterXSS(descriptionTextArea.text), - colorPicker.text, - popup.selectedImage) + membershipRequirementSettingPopup.checkedMembership, + ensOnlySwitch.switchChecked, + popup.selectedImage, + imageDimensions.aX, + imageDimensions.aY, + imageDimensions.bX, + imageDimensions.bY) } if (error) { diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRadioButton.qml b/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRadioButton.qml new file mode 100644 index 0000000000..35f486f27e --- /dev/null +++ b/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRadioButton.qml @@ -0,0 +1,49 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtGraphicalEffects 1.13 +import "../../../../shared" +import "../../../../shared/status" +import "../../../../imports" +import "." + +Item { + property string text + property string description + property var buttonGroup + property bool checked: false + property bool hideSeparator: false + signal radioCheckedChanged(bool checked) + + id: root + width: parent.width + height: childrenRect.height + + StatusRadioButtonRow { + id: radioBtn + text: root.text + buttonGroup: root.buttonGroup + checked: root.checked + onRadioCheckedChanged: { + root.radioCheckedChanged(checked) + } + } + + StyledText { + id: radioDesc + text: root.description + anchors.top: radioBtn.bottom + anchors.topMargin: Style.current.halfPadding + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 100 + font.pixelSize: 13 + color: Style.current.secondaryText + wrapMode: Text.WordWrap + } + + Separator { + visible: !root.hideSeparator + anchors.top: radioDesc.bottom + anchors.topMargin: visible ? Style.current.halfPadding : 0 + } +} diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequestsPopup.qml b/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequestsPopup.qml new file mode 100644 index 0000000000..49103985bb --- /dev/null +++ b/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequestsPopup.qml @@ -0,0 +1,165 @@ +import QtQuick 2.12 +import "../../../../imports" +import "../../../../shared" +import "../../../../shared/status" + +ModalPopup { + property alias membershipRequestList: membershipRequestList + + id: popup + + onOpened: { + errorText.text = "" + } + + header: Item { + height: 60 + width: parent.width + + StyledText { + id: titleText + text: qsTr("Membership requests") + anchors.top: parent.top + anchors.topMargin: 2 + anchors.left: parent.left + font.bold: true + font.pixelSize: 17 + wrapMode: Text.WordWrap + } + + StyledText { + id: nbRequestsText + text: membershipRequestList.count + width: 160 + anchors.left: titleText.left + anchors.top: titleText.bottom + anchors.topMargin: 2 + font.pixelSize: 15 + color: Style.current.darkGrey + } + + Separator { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: -Style.current.padding + anchors.leftMargin: -Style.current.padding + } + } + + Item { + anchors.fill: parent + + StyledText { + id: errorText + visible: !!text + height: visible ? implicitHeight : 0 + color: Style.current.danger + anchors.top: parent.top + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + anchors.topMargin: visible ? Style.current.smallPadding : 0 + width: parent.width + } + + ListView { + id: membershipRequestList + model: chatsModel.activeCommunity.communityMembershipRequests + anchors.top: errorText.bottom + anchors.topMargin: Style.current.smallPadding + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: -Style.current.xlPadding + anchors.leftMargin: -Style.current.xlPadding + height: parent.height + + delegate: Item { + property int contactIndex: profileModel.contacts.list.getContactIndexByPubkey(publicKey) + property string identicon: utilsModel.generateIdenticon(publicKey) + property string profileImage: contactIndex === -1 ? identicon : + profileModel.contacts.list.rowData(contactIndex, 'thumbnailImage') || identicon + property string displayName: Utils.getDisplayName(publicKey, contactIndex) + + id: requestLine + height: 52 + width: parent.width + + StatusImageIdenticon { + id: accountImage + width: 36 + height: 36 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + source: requestLine.profileImage + anchors.leftMargin: Style.current.padding + } + + StyledText { + text: requestLine.displayName + anchors.left: accountImage.right + anchors.leftMargin: Style.current.padding + anchors.right: thumbsUp.left + anchors.rightMargin: Style.current.padding + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: 15 + color: Style.current.darkGrey + } + + SVGImage { + id: thumbsUp + source: "../../../img/thumbsUp.svg" + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + anchors.right: thumbsDown.left + anchors.rightMargin: Style.current.padding + width: 28 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + errorText.text = "" + const error = chatsModel.acceptRequestToJoinCommunity(id) + if (error) { + errorText.text = error + } + } + } + } + + SVGImage { + id: thumbsDown + source: "../../../img/thumbsDown.svg" + fillMode: Image.PreserveAspectFit + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: Style.current.padding + width: 28 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + errorText.text = "" + const error = chatsModel.declineRequestToJoinCommunity(id) + if (error) { + errorText.text = error + } + } + } + } + } + + } + } + + footer: StatusRoundButton { + id: btnBack + anchors.left: parent.left + icon.name: "arrow-right" + icon.width: 20 + icon.height: 16 + rotation: 180 + onClicked: popup.close() + } +} diff --git a/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequirementPopup.qml b/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequirementPopup.qml new file mode 100644 index 0000000000..0811239e0f --- /dev/null +++ b/ui/app/AppLayouts/Chat/CommunityComponents/MembershipRequirementPopup.qml @@ -0,0 +1,101 @@ +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 int checkedMembership: Constants.communityChatPublicAccess + + id: popup + height: 600 + + title: qsTr("Membership requirement") + + ScrollView { + property ScrollBar vScrollBar: ScrollBar.vertical + + id: scrollView + anchors.fill: parent + rightPadding: Style.current.bigPadding + anchors.rightMargin: - Style.current.bigPadding + leftPadding: Style.current.bigPadding + anchors.leftMargin: - Style.current.bigPadding + contentHeight: content.height + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + clip: true + + ButtonGroup { + id: membershipRequirementGroup + } + + Column { + id: content + width: parent.width + spacing: Style.current.padding + + MembershipRadioButton { + text: qsTr("Require approval") + description: qsTr("Your community is free to join, but new members are required to be approved by the community creator first") + buttonGroup: membershipRequirementGroup + checked: popup.checkedMembership === Constants.communityChatOnRequestAccess + onRadioCheckedChanged: { + if (checked) { + popup.checkedMembership = Constants.communityChatOnRequestAccess + } + } + } + + MembershipRadioButton { + text: qsTr("Require invite from another member") + description: qsTr("Your community can only be joined by an invitation from existing community members") + buttonGroup: membershipRequirementGroup + checked: popup.checkedMembership === Constants.communityChatInvitationOnlyAccess + onRadioCheckedChanged: { + if (checked) { + popup.checkedMembership = Constants.communityChatInvitationOnlyAccess + } + } + } + + // This should be a check box +// MembershipRadioButton { +// text: qsTr("Require ENS username") +// description: qsTr("Your community requires an ENS username to be able to join") +// buttonGroup: membershipRequirementGroup +// } + + MembershipRadioButton { + text: qsTr("No requirement") + description: qsTr("Your community is free for anyone to join") + buttonGroup: membershipRequirementGroup + hideSeparator: true + checked: popup.checkedMembership === Constants.communityChatPublicAccess + onRadioCheckedChanged: { + if (checked) { + popup.checkedMembership = Constants.communityChatPublicAccess + } + } + } + } + } + + footer: 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: { + popup.close() + } + } +} + diff --git a/ui/app/AppLayouts/Profile/Sections/ChangeProfilePicModal.qml b/ui/app/AppLayouts/Profile/Sections/ChangeProfilePicModal.qml index cf0524c898..7cb4c7ad5f 100644 --- a/ui/app/AppLayouts/Profile/Sections/ChangeProfilePicModal.qml +++ b/ui/app/AppLayouts/Profile/Sections/ChangeProfilePicModal.qml @@ -54,51 +54,12 @@ ModalPopup { color: Style.current.danger } - ModalPopup { + ImageCropperModal { id: cropImageModal - width: image.width + 50 - height: image.height + 170 - //% "Crop your image (optional)" - title: qsTrId("crop-your-image--optional-") - Image { - id: image - width: 400 - source: popup.selectedImage - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - fillMode: Image.PreserveAspectFit - } - - ImageCropper { - id: imageCropper - x: image.x - y: image.y - image: image - } - - footer: StatusButton { - id: doUploadBtn - //% "Finish" - text: qsTrId("finish") - anchors.right: parent.right - anchors.bottom: parent.bottom - onClicked: { - const aXPercent = imageCropper.selectorRectangle.x / image.width - const aYPercent = imageCropper.selectorRectangle.y / image.height - const bXPercent = (imageCropper.selectorRectangle.x + imageCropper.selectorRectangle.width) / image.width - const bYPercent = (imageCropper.selectorRectangle.y + imageCropper.selectorRectangle.height) / image.height - - - const aX = Math.round(aXPercent * image.sourceSize.width) - const aY = Math.round(aYPercent * image.sourceSize.height) - - const bX = Math.round(bXPercent * image.sourceSize.width) - const bY = Math.round(bYPercent * image.sourceSize.height) - - uploadError = profileModel.uploadNewProfilePic(selectedImage, aX, aY, bX, bY) - cropImageModal.close() - } + selectedImage: popup.selectedImage + onCropFinished: { + uploadError = profileModel.uploadNewProfilePic(selectedImage, aX, aY, bX, bY) } } } diff --git a/ui/app/AppLayouts/Timeline/TimelineLayout.qml b/ui/app/AppLayouts/Timeline/TimelineLayout.qml index dbe36478b7..6ec1210e2b 100644 --- a/ui/app/AppLayouts/Timeline/TimelineLayout.qml +++ b/ui/app/AppLayouts/Timeline/TimelineLayout.qml @@ -111,52 +111,12 @@ ScrollView { section.criteria: ViewSection.FullString } - DelegateModel { + DelegateModelGeneralized { id: messageListDelegate - property var moreThan: [ + lessThan: [ 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 { diff --git a/ui/app/img/thumbsDown.svg b/ui/app/img/thumbsDown.svg new file mode 100644 index 0000000000..bf396dd9c3 --- /dev/null +++ b/ui/app/img/thumbsDown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/app/img/thumbsUp.svg b/ui/app/img/thumbsUp.svg new file mode 100644 index 0000000000..6c4ac8f8d7 --- /dev/null +++ b/ui/app/img/thumbsUp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/imports/Utils.qml b/ui/imports/Utils.qml index 054f83928c..26028c5cbf 100644 --- a/ui/imports/Utils.qml +++ b/ui/imports/Utils.qml @@ -30,6 +30,24 @@ QtObject { (!startsWith0x(value) && value.length === 64)) } + function getDisplayName(publicKey, contactIndex) { + if (contactIndex === undefined) { + contactIndex = profileModel.contacts.list.getContactIndexByPubkey(publicKey) + } + + if (contactIndex === -1) { + return utilsModel.generateAlias(publicKey) + } + const ensVerified = profileModel.contacts.list.rowData(contactIndex, 'ensVerified') + if (!ensVerified) { + const nickname = profileModel.contacts.list.rowData(contactIndex, 'localNickname') + if (nickname) { + return nickname + } + } + return profileModel.contacts.list.rowData(contactIndex, 'name') + } + function isMnemonic(value) { if(!value.match(/^([a-z\s]+)$/)){ return false; diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index a3af5ec2a6..3cde22aceb 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -136,6 +136,9 @@ DISTFILES += \ app/AppLayouts/Chat/CommunityComponents/CreateCommunityPopup.qml \ app/AppLayouts/Chat/CommunityComponents/ImportCommunityPopup.qml \ app/AppLayouts/Chat/CommunityComponents/InviteFriendsToCommunityPopup.qml \ + app/AppLayouts/Chat/CommunityComponents/MembershipRadioButton.qml \ + app/AppLayouts/Chat/CommunityComponents/MembershipRequestsPopup.qml \ + app/AppLayouts/Chat/CommunityComponents/MembershipRequirementPopup.qml \ app/AppLayouts/Chat/ContactsColumn/AddChat.qml \ app/AppLayouts/Chat/ContactsColumn/ClosedEmptyView.qml \ app/AppLayouts/Chat/components/ChooseBrowserPopup.qml \ @@ -354,9 +357,11 @@ DISTFILES += \ shared/AddButton.qml \ shared/Address.qml \ shared/CropCornerRectangle.qml \ + shared/DelegateModelGeneralized.qml \ shared/FormGroup.qml \ shared/IconButton.qml \ shared/ImageCropper.qml \ + shared/ImageCropperModal.qml \ shared/Input.qml \ shared/LabelValueRow.qml \ shared/ModalPopup.qml \ diff --git a/ui/shared/DelegateModelGeneralized.qml b/ui/shared/DelegateModelGeneralized.qml new file mode 100644 index 0000000000..a953917587 --- /dev/null +++ b/ui/shared/DelegateModelGeneralized.qml @@ -0,0 +1,50 @@ +import QtQuick 2.13 +import QtQml.Models 2.3 +import "../imports" + +DelegateModel { + id: delegateModel + property var lessThan + + property int sortOrder: 0 + onSortOrderChanged: items.setGroups(0, items.count, "unsorted") + + function insertPosition(lessThan, item) { + var lower = 0 + var upper = items.count + while (lower < upper) { + var middle = Math.floor(lower + (upper - lower) / 2) + var result = lessThan(item.model, items.get(middle).model); + if (result) { + upper = middle + } else { + lower = middle + 1 + } + } + return lower + } + + function sort(lessThan) { + while (unsortedItems.count > 0) { + var item = unsortedItems.get(0) + var index = insertPosition(lessThan, item) + + item.groups = "items" + items.move(item.itemsIndex, index) + } + } + + items.includeByDefault: false + groups: DelegateModelGroup { + id: unsortedItems + name: "unsorted" + + includeByDefault: true + onChanged: { + if (delegateModel.sortOrder === delegateModel.lessThan.length) + setGroups(0, count, "items") + else + delegateModel.sort(delegateModel.lessThan[delegateModel.sortOrder]) + } + } +} diff --git a/ui/shared/ImageCropperModal.qml b/ui/shared/ImageCropperModal.qml new file mode 100644 index 0000000000..1624d7833a --- /dev/null +++ b/ui/shared/ImageCropperModal.qml @@ -0,0 +1,54 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 +import "../imports" +import "./status" + +ModalPopup { + property string selectedImage + signal cropFinished(aX: int, aY: int, bX: int, bY: int) + + id: cropImageModal + width: image.width + 50 + height: image.height + 170 + title: qsTr("Crop your image (optional)") + + Image { + id: image + width: 400 + source: cropImageModal.selectedImage + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.PreserveAspectFit + } + + ImageCropper { + id: imageCropper + x: image.x + y: image.y + image: image + } + + footer: StatusButton { + id: doneBtn + text: qsTr("Finish") + anchors.right: parent.right + anchors.bottom: parent.bottom + onClicked: { + const aXPercent = imageCropper.selectorRectangle.x / image.width + const aYPercent = imageCropper.selectorRectangle.y / image.height + const bXPercent = (imageCropper.selectorRectangle.x + imageCropper.selectorRectangle.width) / image.width + const bYPercent = (imageCropper.selectorRectangle.y + imageCropper.selectorRectangle.height) / image.height + + + const aX = Math.round(aXPercent * image.sourceSize.width) + const aY = Math.round(aYPercent * image.sourceSize.height) + + const bX = Math.round(bXPercent * image.sourceSize.width) + const bY = Math.round(bYPercent * image.sourceSize.height) + + cropImageModal.cropFinished(aX, aY, bX, bY) + cropImageModal.close() + } + } +} diff --git a/vendor/status-go b/vendor/status-go index 363ab0a2ab..09942bf200 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 363ab0a2ab8dce98193c899c2c202cc885a45143 +Subproject commit 09942bf200bd90bf3f36c5db2a911644fb6822e7