diff --git a/src/app/modules/main/communities/controller.nim b/src/app/modules/main/communities/controller.nim index 2663b5faac..ba24922e4f 100644 --- a/src/app/modules/main/communities/controller.nim +++ b/src/app/modules/main/communities/controller.nim @@ -58,6 +58,10 @@ proc init*(self: Controller) = self.delegate.communityEdited(community) self.delegate.curatedCommunityEdited(CuratedCommunity(communityId: community.id, available: true, community:community)) + self.events.on(SIGNAL_COMMUNITY_MUTED) do(e:Args): + let args = CommunityMutedArgs(e) + self.delegate.communityMuted(args.communityId, args.muted) + proc getAllCommunities*(self: Controller): seq[CommunityDto] = result = self.communityService.getAllCommunities() diff --git a/src/app/modules/main/communities/io_interface.nim b/src/app/modules/main/communities/io_interface.nim index 08e0a6c4d9..59eb954b49 100644 --- a/src/app/modules/main/communities/io_interface.nim +++ b/src/app/modules/main/communities/io_interface.nim @@ -99,3 +99,6 @@ method onImportCommunityErrorOccured*(self: AccessInterface, error: string) {.ba method viewDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") + +method communityMuted*(self: AccessInterface, communityId: string, muted: bool) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/communities/module.nim b/src/app/modules/main/communities/module.nim index 4d586ed29b..2fbde772b4 100644 --- a/src/app/modules/main/communities/module.nim +++ b/src/app/modules/main/communities/module.nim @@ -93,6 +93,7 @@ method getCommunityItem(self: Module, c: CommunityDto): SectionItem = c.isMember, c.permissions.access, c.permissions.ensOnly, + c.muted, c.members.map(proc(member: Member): user_item.Item = let contactDetails = self.controller.getContactDetails(member.id) result = user_item.initItem( @@ -185,6 +186,9 @@ method reorderCommunityCategories*(self: Module, communityId: string, categoryId # self.controller.reorderCommunityCategories(communityId, categoryId, position) discard +method communityMuted*(self: Module, communityId: string, muted: bool) = + self.view.model().setMuted(communityId, muted) + method requestToJoinCommunity*(self: Module, communityId: string, ensName: string) = self.controller.requestToJoinCommunity(communityId, ensName) diff --git a/src/app/modules/main/communities/view.nim b/src/app/modules/main/communities/view.nim index 49ca54b744..5caed83de9 100644 --- a/src/app/modules/main/communities/view.nim +++ b/src/app/modules/main/communities/view.nim @@ -123,3 +123,9 @@ QtObject: proc importingCommunityStateChanged*(self:View, state: int, errorMsg: string) {.signal.} proc emitImportingCommunityStateChangedSignal*(self: View, state: int, errorMsg: string) = self.importingCommunityStateChanged(state, errorMsg) + + proc isMemberOfCommunity*(self: View, communityId: string, pubKey: string): bool {.slot.} = + let sectionItem = self.model.getItemById(communityId) + if (section_item.id == ""): + return false + return sectionItem.hasMember(pubKey) \ No newline at end of file diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index f9c33c3927..829cd82696 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -152,7 +152,7 @@ proc newModule*[T]( result.profileSectionModule = profile_section_module.newModule( result, events, accountsService, settingsService, stickersService, profileService, contactsService, aboutService, languageService, privacyService, nodeConfigurationService, - devicesService, mailserversService, chatService, ensService, walletAccountService, generalService + devicesService, mailserversService, chatService, ensService, walletAccountService, generalService, communityService ) result.stickersModule = stickers_module.newModule(result, events, stickersService, settingsService, walletAccountService) result.activityCenterModule = activity_center_module.newModule(result, events, activityCenterService, contactsService, @@ -217,6 +217,7 @@ proc createChannelGroupItem[T](self: Module[T], c: ChannelGroupDto): SectionItem if (isCommunity): communityDetails.isMember else: true, c.permissions.access, c.permissions.ensOnly, + c.muted, c.members.map(proc(member: ChatMember): user_item.Item = let contactDetails = self.controller.getContactDetails(member.id) result = user_item.initItem( diff --git a/src/app/modules/main/profile_section/communities/controller.nim b/src/app/modules/main/profile_section/communities/controller.nim new file mode 100644 index 0000000000..56d07b6697 --- /dev/null +++ b/src/app/modules/main/profile_section/communities/controller.nim @@ -0,0 +1,28 @@ +import io_interface + +import ../../../../../app_service/service/community/service as community_service + + +type + Controller* = ref object of RootObj + delegate: io_interface.AccessInterface + communityService: community_service.Service + +proc newController*(delegate: io_interface.AccessInterface, + communityService: community_service.Service): Controller = + result = Controller() + result.delegate = delegate + result.communityService = communityService + +proc delete*(self: Controller) = + discard + +proc inviteUsersToCommunity*(self: Controller, communityID: string, pubKeys: string): string = + result = self.communityService.inviteUsersToCommunityById(communityID, pubKeys) + +proc leaveCommunity*(self: Controller, communityID: string) = + self.communityService.leaveCommunity(communityID) + +method setCommunityMuted*(self: Controller, communityID: string, muted: bool) = + self.communityService.setCommunityMuted(communityID, muted) + diff --git a/src/app/modules/main/profile_section/communities/io_interface.nim b/src/app/modules/main/profile_section/communities/io_interface.nim new file mode 100644 index 0000000000..05f8b51c80 --- /dev/null +++ b/src/app/modules/main/profile_section/communities/io_interface.nim @@ -0,0 +1,33 @@ +import NimQml + +type + AccessInterface* {.pure inheritable.} = ref object of RootObj + ## Abstract class for any input/interaction with this module. + +method delete*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method load*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method isLoaded*(self: AccessInterface): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") + +# View Delegate Interface +# Delegate for the view must be declared here due to use of QtObject and multi +# inheritance, which is not well supported in Nim. +method viewDidLoad*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") + +method inviteUsersToCommunity*(self: AccessInterface, communityID: string, pubKeysJSON: string): string {.base.} = + raise newException(ValueError, "No implementation available") + +method leaveCommunity*(self: AccessInterface, communityID: string) {.base.} = + raise newException(ValueError, "No implementation available") + +method setCommunityMuted*(self: AccessInterface, communityID: string, muted: bool) {.base.} = + raise newException(ValueError, "No implementation available") + diff --git a/src/app/modules/main/profile_section/communities/module.nim b/src/app/modules/main/profile_section/communities/module.nim new file mode 100644 index 0000000000..ff8c444153 --- /dev/null +++ b/src/app/modules/main/profile_section/communities/module.nim @@ -0,0 +1,52 @@ +import NimQml, chronicles + +import ./io_interface, ./view, ./controller +import ../io_interface as delegate_interface + +import ../../../../core/eventemitter +import ../../../../../app_service/service/community/service as community_service + +export io_interface + +type + Module* = ref object of io_interface.AccessInterface + delegate: delegate_interface.AccessInterface + controller: Controller + view: View + viewVariant: QVariant + moduleLoaded: bool + +proc newModule*(delegate: delegate_interface.AccessInterface, + communityService: community_service.Service): + Module = + result = Module() + result.delegate = delegate + result.view = newView(result) + result.viewVariant = newQVariant(result.view) + result.controller = controller.newController(result, communityService) + result.moduleLoaded = false + +method delete*(self: Module) = + self.view.delete + +method load*(self: Module) = + self.view.load() + +method isLoaded*(self: Module): bool = + return self.moduleLoaded + +method viewDidLoad*(self: Module) = + self.moduleLoaded = true + self.delegate.communitiesModuleDidLoad() + +method getModuleAsVariant*(self: Module): QVariant = + return self.viewVariant + +method inviteUsersToCommunity*(self: Module, communityID: string, pubKeysJSON: string): string = + result = self.controller.inviteUsersToCommunity(communityID, pubKeysJSON) + +method leaveCommunity*(self: Module, communityID: string) = + self.controller.leaveCommunity(communityID) + +method setCommunityMuted*(self: Module, communityID: string, muted: bool) = + self.controller.setCommunityMuted(communityID, muted) diff --git a/src/app/modules/main/profile_section/communities/view.nim b/src/app/modules/main/profile_section/communities/view.nim new file mode 100644 index 0000000000..3afc65fe11 --- /dev/null +++ b/src/app/modules/main/profile_section/communities/view.nim @@ -0,0 +1,29 @@ +import NimQml + +import ./io_interface + + +QtObject: + type + View* = ref object of QObject + delegate: io_interface.AccessInterface + + proc delete*(self: View) = + self.QObject.delete + + proc newView*(delegate: io_interface.AccessInterface): View = + new(result, delete) + result.QObject.setup + result.delegate = delegate + + proc load*(self: View) = + self.delegate.viewDidLoad() + + method inviteUsersToCommunity*(self: View, communityID: string, pubKeysJSON: string): string {.slot.} = + result = self.delegate.inviteUsersToCommunity(communityID, pubKeysJSON) + + method leaveCommunity*(self: View, communityID: string) {.slot.} = + self.delegate.leaveCommunity(communityID) + + method setCommunityMuted*(self: View, communityID: string, muted: bool) {.slot.} = + self.delegate.setCommunityMuted(communityID, muted) \ No newline at end of file diff --git a/src/app/modules/main/profile_section/io_interface.nim b/src/app/modules/main/profile_section/io_interface.nim index 53f254ec86..2f0d19e182 100644 --- a/src/app/modules/main/profile_section/io_interface.nim +++ b/src/app/modules/main/profile_section/io_interface.nim @@ -78,4 +78,10 @@ method ensUsernamesModuleDidLoad*(self: AccessInterface) {.base.} = raise newException(ValueError, "No implementation available") method getEnsUsernamesModule*(self: AccessInterface): QVariant {.base.} = - raise newException(ValueError, "No implementation available") \ No newline at end of file + raise newException(ValueError, "No implementation available") + +method getCommunitiesModule*(self: AccessInterface): QVariant {.base.} = + raise newException(ValueError, "No implementation available") + +method communitiesModuleDidLoad*(self: AccessInterface) {.base.} = + raise newException(ValueError, "No implementation available") diff --git a/src/app/modules/main/profile_section/module.nim b/src/app/modules/main/profile_section/module.nim index cd270be341..73c228354d 100644 --- a/src/app/modules/main/profile_section/module.nim +++ b/src/app/modules/main/profile_section/module.nim @@ -18,6 +18,8 @@ import ../../../../app_service/service/stickers/service as stickersService import ../../../../app_service/service/ens/service as ens_service import ../../../../app_service/service/wallet_account/service as wallet_account_service import ../../../../app_service/service/general/service as general_service +import ../../../../app_service/service/community/service as community_service + import ./profile/module as profile_module import ./contacts/module as contacts_module @@ -29,6 +31,7 @@ import ./devices/module as devices_module import ./sync/module as sync_module import ./notifications/module as notifications_module import ./ens_usernames/module as ens_usernames_module +import ./communities/module as communities_module export io_interface @@ -50,6 +53,7 @@ type syncModule: sync_module.AccessInterface notificationsModule: notifications_module.AccessInterface ensUsernamesModule: ens_usernames_module.AccessInterface + communitiesModule: communities_module.AccessInterface proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, @@ -67,7 +71,8 @@ proc newModule*(delegate: delegate_interface.AccessInterface, chatService: chat_service.Service, ensService: ens_service.Service, walletAccountService: wallet_account_service.Service, - generalService: general_service.Service + generalService: general_service.Service, + communityService: community_service.Service ): Module = result = Module() result.delegate = delegate @@ -88,6 +93,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, result.ensUsernamesModule = ens_usernames_module.newModule( result, events, settingsService, ensService, walletAccountService ) + result.communitiesModule = communities_module.newModule(result, communityService) singletonInstance.engine.setRootContextProperty("profileSectionModule", result.viewVariant) @@ -100,6 +106,7 @@ method delete*(self: Module) = self.advancedModule.delete self.devicesModule.delete self.syncModule.delete + self.communitiesModule.delete self.view.delete self.viewVariant.delete @@ -117,6 +124,7 @@ method load*(self: Module) = self.syncModule.load() self.notificationsModule.load() self.ensUsernamesModule.load() + self.communitiesModule.load() method isLoaded*(self: Module): bool = return self.moduleLoaded @@ -152,6 +160,9 @@ proc checkIfModuleDidLoad(self: Module) = if(not self.ensUsernamesModule.isLoaded()): return + if(not self.communitiesModule.isLoaded()): + return + self.moduleLoaded = true self.delegate.profileSectionDidLoad() @@ -214,3 +225,9 @@ method ensUsernamesModuleDidLoad*(self: Module) = method getEnsUsernamesModule*(self: Module): QVariant = self.ensUsernamesModule.getModuleAsVariant() + +method getCommunitiesModule*(self: Module): QVariant = + self.communitiesModule.getModuleAsVariant() + +method communitiesModuleDidLoad*(self: Module) = + self.checkIfModuleDidLoad() \ No newline at end of file diff --git a/src/app/modules/main/profile_section/view.nim b/src/app/modules/main/profile_section/view.nim index e93f4eb64d..99c6966d0a 100644 --- a/src/app/modules/main/profile_section/view.nim +++ b/src/app/modules/main/profile_section/view.nim @@ -65,3 +65,8 @@ QtObject: return self.delegate.getEnsUsernamesModule() QtProperty[QVariant] ensUsernamesModule: read = getEnsUsernamesModule + + proc getCommunitiesModule(self: View): QVariant {.slot.} = + return self.delegate.getCommunitiesModule() + QtProperty[QVariant] communitiesModule: + read = getCommunitiesModule diff --git a/src/app/modules/shared_models/section_item.nim b/src/app/modules/shared_models/section_item.nim index 8186bb9d87..f7b462b094 100644 --- a/src/app/modules/shared_models/section_item.nim +++ b/src/app/modules/shared_models/section_item.nim @@ -34,6 +34,7 @@ type canRequestAccess: bool access: int ensOnly: bool + muted: bool membersModel: user_model.Model pendingRequestsToJoinModel: PendingRequestModel historyArchiveSupportEnabled: bool @@ -60,6 +61,7 @@ proc initItem*( isMember = false, access: int = 0, ensOnly = false, + muted = false, members: seq[user_item.Item] = @[], pendingRequestsToJoin: seq[PendingRequestItem] = @[], historyArchiveSupportEnabled = false, @@ -85,6 +87,7 @@ proc initItem*( result.isMember = isMember result.access = access result.ensOnly = ensOnly + result.muted = muted result.membersModel = newModel() result.membersModel.setItems(members) result.pendingRequestsToJoinModel = newPendingRequestModel() @@ -117,6 +120,7 @@ proc `$`*(self: SectionItem): string = isMember:{self.isMember}, access:{self.access}, ensOnly:{self.ensOnly}, + muted:{self.muted}, members:{self.membersModel}, historyArchiveSupportEnabled:{self.historyArchiveSupportEnabled}, pinMessageAllMembersEnabled:{self.pinMessageAllMembersEnabled}, @@ -194,6 +198,12 @@ proc access*(self: SectionItem): int {.inline.} = proc ensOnly*(self: SectionItem): bool {.inline.} = self.ensOnly +proc muted*(self: SectionItem): bool {.inline.} = + self.muted + +proc `muted=`*(self: var SectionItem, value: bool) {.inline.} = + self.muted = value + proc members*(self: SectionItem): user_model.Model {.inline.} = self.membersModel diff --git a/src/app/modules/shared_models/section_model.nim b/src/app/modules/shared_models/section_model.nim index 290637f4bb..fc820ef39b 100644 --- a/src/app/modules/shared_models/section_model.nim +++ b/src/app/modules/shared_models/section_model.nim @@ -26,6 +26,7 @@ type CanRequestAccess Access EnsOnly + Muted MembersModel PendingRequestsToJoinModel HistoryArchiveSupportEnabled @@ -87,6 +88,7 @@ QtObject: ModelRole.CanRequestAccess.int:"canRequestAccess", ModelRole.Access.int:"access", ModelRole.EnsOnly.int:"ensOnly", + ModelRole.Muted.int:"muted", ModelRole.MembersModel.int:"members", ModelRole.PendingRequestsToJoinModel.int:"pendingRequestsToJoin", ModelRole.HistoryArchiveSupportEnabled.int:"historyArchiveSupportEnabled", @@ -144,6 +146,8 @@ QtObject: result = newQVariant(item.access) of ModelRole.EnsOnly: result = newQVariant(item.ensOnly) + of ModelRole.Muted: + result = newQVariant(item.muted) of ModelRole.MembersModel: result = newQVariant(item.members) of ModelRole.PendingRequestsToJoinModel: @@ -204,6 +208,31 @@ QtObject: self.countChanged() + proc setMuted*(self: SectionModel, id: string, muted: bool) = + let index = self.getItemIndex(id) + if (index == -1): + return + + self.items[index].muted = muted + let dataIndex = self.createIndex(index, 0, nil) + self.dataChanged(dataIndex, dataIndex, @[ + ModelRole.Name.int, + ModelRole.Description.int, + ModelRole.Image.int, + ModelRole.Icon.int, + ModelRole.Color.int, + ModelRole.HasNotification.int, + ModelRole.NotificationsCount.int, + ModelRole.IsMember.int, + ModelRole.CanJoin.int, + ModelRole.Joined.int, + ModelRole.Muted.int, + ModelRole.MembersModel.int, + ModelRole.PendingRequestsToJoinModel.int, + ModelRole.HistoryArchiveSupportEnabled.int + ]) + + proc editItem*(self: SectionModel, item: SectionItem) = let index = self.getItemIndex(item.id) if (index == -1): @@ -223,6 +252,7 @@ QtObject: ModelRole.IsMember.int, ModelRole.CanJoin.int, ModelRole.Joined.int, + ModelRole.Muted.int, ModelRole.MembersModel.int, ModelRole.PendingRequestsToJoinModel.int, ModelRole.HistoryArchiveSupportEnabled.int, diff --git a/src/app_service/service/community/service.nim b/src/app_service/service/community/service.nim index a3846b6a85..26980708cb 100644 --- a/src/app_service/service/community/service.nim +++ b/src/app_service/service/community/service.nim @@ -65,6 +65,10 @@ type communityId*: string pubKey*: string + CommunityMutedArgs* = ref object of Args + communityId*: string + muted*: bool + # Signals which may be emitted by this service: const SIGNAL_COMMUNITY_JOINED* = "communityJoined" const SIGNAL_COMMUNITY_MY_REQUEST_ADDED* = "communityMyRequestAdded" @@ -88,6 +92,7 @@ const SIGNAL_COMMUNITY_MEMBER_APPROVED* = "communityMemberApproved" const SIGNAL_COMMUNITY_MEMBER_REMOVED* = "communityMemberRemoved" const SIGNAL_NEW_REQUEST_TO_JOIN_COMMUNITY* = "newRequestToJoinCommunity" const SIGNAL_CURATED_COMMUNITY_FOUND* = "curatedCommunityFound" +const SIGNAL_COMMUNITY_MUTED* = "communityMuted" QtObject: type @@ -1030,6 +1035,9 @@ QtObject: proc setCommunityMuted*(self: Service, communityId: string, muted: bool) = try: discard status_go.setCommunityMuted(communityId, muted) + + self.events.emit(SIGNAL_COMMUNITY_MUTED, + CommunityMutedArgs(communityId: communityId, muted: muted)) except Exception as e: error "Error setting community un/muted", msg = e.msg diff --git a/ui/app/AppLayouts/Chat/panels/communities/CommunityProfilePopupInviteFriendsPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/CommunityProfilePopupInviteFriendsPanel.qml index d8c31802ff..0d1b99b0b9 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/CommunityProfilePopupInviteFriendsPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/communities/CommunityProfilePopupInviteFriendsPanel.qml @@ -23,17 +23,6 @@ Column { property var community property alias contactListSearch: contactFieldAndList - function sendInvites(pubKeys) { - const error = communitySectionModule.inviteUsersToCommunity(JSON.stringify(pubKeys)) - if (error) { - console.error('Error inviting', error) - contactFieldAndList.validationError = error - return - } - //% "Invite successfully sent" - contactFieldAndList.successMessage = qsTrId("invite-successfully-sent") - } - StatusDescriptionListItem { //% "Share community" title: qsTrId("share-community") @@ -66,6 +55,7 @@ Column { anchors.horizontalCenter: parent.horizontalCenter width: parent.width - 32 contactsStore: root.contactsStore + communityModule: root.communitySectionModule community: root.community showCheckbox: true hideCommunityMembers: true diff --git a/ui/app/AppLayouts/Chat/popups/community/InviteFriendsToCommunityPopup.qml b/ui/app/AppLayouts/Chat/popups/community/InviteFriendsToCommunityPopup.qml index 5dbd6654b7..4877049cc7 100644 --- a/ui/app/AppLayouts/Chat/popups/community/InviteFriendsToCommunityPopup.qml +++ b/ui/app/AppLayouts/Chat/popups/community/InviteFriendsToCommunityPopup.qml @@ -21,6 +21,8 @@ StatusModal { property var communitySectionModule property bool hasAddedContacts + signal sendInvites(var pubKeys) + onOpened: { contentItem.community = community; @@ -36,6 +38,16 @@ StatusModal { //% "Invite friends" header.title: qsTrId("invite-friends") + function proccesInviteResult(error) { + if (error) { + console.error('Error inviting', error) + contactFieldAndList.validationError = error + return + } + //% "Invite successfully sent" + popup.contentItem.contactListSearch.successMessage = qsTrId("invite-successfully-sent") + } + contentItem: CommunityProfilePopupInviteFriendsPanel { id: contactFieldAndList rootStore: popup.rootStore @@ -62,7 +74,7 @@ StatusModal { //% "Invite" text: qsTrId("invite-button") onClicked : { - popup.contentItem.sendInvites(popup.contentItem.contactListSearch.pubKeys) + popup.sendInvites(popup.contentItem.contactListSearch.pubKeys) } } ] diff --git a/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml b/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml index d77044a75e..656908f73b 100644 --- a/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ContactsColumnView.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts 1.13 import utils 1.0 import shared 1.0 +import shared.popups 1.0 import "../panels" import "../popups" @@ -329,7 +330,7 @@ Item { Component { id: importCommunitiesPopupComponent - AccessExistingCommunityPopup { + ImportCommunityPopup { anchors.centerIn: parent store: root.store onClosed: { diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml index e9b1d51dde..3a5724fc52 100644 --- a/ui/app/AppLayouts/Profile/ProfileLayout.qml +++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml @@ -213,6 +213,17 @@ StatusAppTwoPanelLayout { sectionTitle: profileView.store.getNameForSubsection(Constants.settingsSubsection.about) contentWidth: d.contentWidth } + + CommunitiesView { + Layout.fillWidth: true + Layout.fillHeight: true + + profileSectionStore: profileView.store + rootStore: profileView.globalStore + contactStore: profileView.store.contactsStore + sectionTitle: profileView.store.getNameForSubsection(Constants.settingsSubsection.communitiesSettings) + contentWidth: d.contentWidth + } } } } diff --git a/ui/app/AppLayouts/Profile/panels/CommunitiesListPanel.qml b/ui/app/AppLayouts/Profile/panels/CommunitiesListPanel.qml new file mode 100644 index 0000000000..ff1e5b65ec --- /dev/null +++ b/ui/app/AppLayouts/Profile/panels/CommunitiesListPanel.qml @@ -0,0 +1,109 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Popups 0.1 + +import utils 1.0 + +ListView { + id: root + + property var communitySectionModule + property var communityProfileModule + property bool hasAddedContacts: false + + signal inviteFriends(var communityData) + + interactive: false + implicitHeight: contentItem.childrenRect.height + spacing: 0 + + delegate: StatusListItem { + id: statusCommunityItem + width: parent.width + title: model.name + subTitle: model.description + tertiaryTitle: qsTr(model.members.count === 1 ?"%1 member" + :"%1 members").arg(model.members.count) + image.source: model.image + icon.isLetterIdenticon: !model.image + icon.background.color: model.color || Theme.palette.primaryColor1 + visible: model.joined + height: visible ? implicitHeight: 0 + + sensor.hoverEnabled: false + + components: [ + StatusFlatButton { + size: StatusBaseButton.Size.Small + type: StatusBaseButton.Type.Danger + border.color: "transparent" + text: qsTrId("leave-community") + onClicked: { + Global.openPopup(leaveCommunityPopup, { + community: model.name, + communityId: model.id + }) + } + }, + StatusFlatRoundButton { + type: StatusFlatRoundButton.Type.Secondary + width: 44 + height: 44 + icon.source: model.muted ? Style.svg("communities/notifications-muted") + : Style.svg("communities/notifications") + onClicked: root.communityProfileModule.setCommunityMuted(model.id, !model.muted) + }, + + StatusFlatRoundButton { + type: StatusFlatRoundButton.Type.Secondary + width: 44 + height: 44 + icon.name: "invite-users" + onClicked: root.inviteFriends(model) + } + ] + } // StatusListItem + + property Component leaveCommunityPopup: StatusModal { + id: leavePopup + property string community: "" + property var communityId + anchors.centerIn: parent + header.title: qsTr("Leave %1").arg(community) + contentItem: Item { + implicitWidth: 368 + implicitHeight: msg.implicitHeight + 32 + StatusBaseText { + id: msg + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 16 + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: qsTr("Are you sure you want to leave? Once you leave, you will have to request to rejoin if you change your mind.") + color: Theme.palette.directColor1 + font.pixelSize: 15 + } + } + + rightButtons: [ + StatusButton { + text: qsTr("Cancel") + onClicked: leavePopup.close() + }, + StatusButton { + type: StatusBaseButton.Type.Danger + text: qsTr("Leave community") + onClicked: { + root.communityProfileModule.leaveCommunity(leavePopup.communityId) + leavePopup.close() + } + } + ] + } +} // ListView diff --git a/ui/app/AppLayouts/Profile/panels/MenuPanel.qml b/ui/app/AppLayouts/Profile/panels/MenuPanel.qml index 2a6b8ff959..d75b0d628a 100644 --- a/ui/app/AppLayouts/Profile/panels/MenuPanel.qml +++ b/ui/app/AppLayouts/Profile/panels/MenuPanel.qml @@ -19,6 +19,8 @@ Column { property bool browserMenuItemEnabled: false property bool walletMenuItemEnabled: false + property bool appsMenuItemsEnabled: false + property bool communitiesMenuItemEnabled: false signal menuItemClicked(var menu_item) @@ -63,9 +65,10 @@ Column { selected: Global.settingsSubsection === model.subsection onClicked: root.menuItemClicked(model) visible: { - (model.subsection !== Constants.settingsSubsection.browserSettings && model.subsection !== Constants.settingsSubsection.wallet) || - (model.subsection === Constants.settingsSubsection.browserSettings && root.browserMenuItemEnabled) || - (model.subsection === Constants.settingsSubsection.wallet && root.walletMenuItemEnabled) + (model.subsection !== Constants.settingsSubsection.browserSettings && model.subsection !== Constants.settingsSubsection.wallet && model.subsection !== Constants.settingsSubsection.communitiesSettings) || + (model.subsection === Constants.settingsSubsection.browserSettings && root.browserMenuItemEnabled) || + (model.subsection === Constants.settingsSubsection.communitiesSettings && root.communitiesMenuItemEnabled) || + (model.subsection === Constants.settingsSubsection.wallet && root.appsMenuItemsEnabled) } badge.value: { switch (model.subsection) { diff --git a/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml b/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml index d60b6813ba..f77af1562b 100644 --- a/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml +++ b/ui/app/AppLayouts/Profile/stores/ProfileSectionStore.qml @@ -62,6 +62,12 @@ QtObject { property bool browserMenuItemEnabled: localAccountSensitiveSettings.isBrowserEnabled property bool walletMenuItemEnabled: localAccountSensitiveSettings.isWalletEnabled + property bool appsMenuItemsEnabled: localAccountSensitiveSettings.isWalletEnabled || localAccountSensitiveSettings.communitiesEnabled + property bool communitiesMenuItemEnabled: localAccountSensitiveSettings.communitiesEnabled + + property var communitiesModuleInst: communitiesModule + property var communitiesList: communitiesModuleInst.model + property var communitiesProfileModule: profileSectionModuleInst.communitiesModule property ListModel mainMenuItems: ListModel { Component.onCompleted: { @@ -88,6 +94,9 @@ QtObject { append({subsection: Constants.settingsSubsection.browserSettings, text: qsTr("Browser"), icon: "browser"}) + append({subsection: Constants.settingsSubsection.communitiesSettings, + text: qsTr("Communities"), + icon: "communities"}) } } @@ -125,6 +134,10 @@ QtObject { } } + function importCommunity(communityKey) { + root.communitiesModuleInst.importCommunity(communityKey); + } + function getCurrentVersion() { return aboutModuleInst.getCurrentVersion() } diff --git a/ui/app/AppLayouts/Profile/views/CommunitiesView.qml b/ui/app/AppLayouts/Profile/views/CommunitiesView.qml new file mode 100644 index 0000000000..e251262dfe --- /dev/null +++ b/ui/app/AppLayouts/Profile/views/CommunitiesView.qml @@ -0,0 +1,95 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 +import StatusQ.Controls 0.1 + +import utils 1.0 +import shared 1.0 +import shared.panels 1.0 +import shared.status 1.0 +import shared.popups 1.0 + +import "../panels" +import "../../Chat/popups/community" + +SettingsContentBase { + id: root + + property var profileSectionStore + property var rootStore + property var contactStore + + clip: true + + titleRowComponentLoader.sourceComponent: StatusButton { + size: StatusBaseButton.Size.Small + text: qsTr("Import community") + onClicked: { + Global.openPopup(importCommunitiesPopupComponent) + } + } + + Item { + id: rootItem + width: root.contentWidth + height: childrenRect.height + + Column { + id: rootLayout + width: parent.width + anchors.top: parent.top + anchors.left: parent.left + + StatusBaseText { + anchors.left: parent.left + anchors.leftMargin: Style.current.padding + color: Theme.palette.baseColor1 + text: qsTr("Communities you've joined") + font.pixelSize: 15 + } + + CommunitiesListPanel { + width: parent.width + model: root.profileSectionStore.communitiesList + communitySectionModule: root.profileSectionStore.communitiesModuleInst + communityProfileModule: root.profileSectionStore.communitiesProfileModule + + onInviteFriends: { + Global.openPopup(inviteFriendsToCommunityPopup, { + community: communityData, + hasAddedContacts: root.contactStore.myContactsModel.count > 0, + communitySectionModule: communityProfileModule + }) + } + } + + } // Column + } // Item + + property Component importCommunitiesPopupComponent: ImportCommunityPopup { + anchors.centerIn: parent + store: root.profileSectionStore + onClosed: { + destroy() + } + } + + property Component inviteFriendsToCommunityPopup: InviteFriendsToCommunityPopup { + anchors.centerIn: parent + rootStore: root.rootStore + contactsStore: root.contactStore + onClosed: { + destroy() + } + + onSendInvites: { + const error = communitySectionModule.inviteUsersToCommunity(communty.id, JSON.stringify(pubKeys)) + processInviteResult(error) + } + } + +} // ScrollView diff --git a/ui/app/AppLayouts/Profile/views/LeftTabView.qml b/ui/app/AppLayouts/Profile/views/LeftTabView.qml index 79941121c3..4f193b1e0b 100644 --- a/ui/app/AppLayouts/Profile/views/LeftTabView.qml +++ b/ui/app/AppLayouts/Profile/views/LeftTabView.qml @@ -41,6 +41,9 @@ Item { appsMenuItems: store.appsMenuItems browserMenuItemEnabled: store.browserMenuItemEnabled walletMenuItemEnabled: store.walletMenuItemEnabled + appsMenuItemsEnabled: store.appsMenuItemsEnabled + communitiesMenuItemEnabled: store.communitiesMenuItemEnabled + onMenuItemClicked: { if (menu_item.subsection === Constants.settingsSubsection.backUpSeed) { Global.openBackUpSeedPopup(); diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 44963aab14..cd62c613ae 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -716,6 +716,11 @@ Item { onClosed: { destroy() } + + onSendInvites: { + const error = communitySectionModule.inviteUsersToCommunity(JSON.stringify(pubKeys)) + processInviteResult(error) + } } } diff --git a/ui/imports/assets/icons/communities/notifications-muted.svg b/ui/imports/assets/icons/communities/notifications-muted.svg new file mode 100644 index 0000000000..d0e34d71e1 --- /dev/null +++ b/ui/imports/assets/icons/communities/notifications-muted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/imports/shared/controls/ContactsListAndSearch.qml b/ui/imports/shared/controls/ContactsListAndSearch.qml index ebea290edb..8d3e4de550 100644 --- a/ui/imports/shared/controls/ContactsListAndSearch.qml +++ b/ui/imports/shared/controls/ContactsListAndSearch.qml @@ -21,6 +21,7 @@ Item { property var rootStore property var contactsStore property var community + property var communityModule property string validationError: "" property string successMessage: "" @@ -181,6 +182,7 @@ Item { contactsStore: root.contactsStore community: root.community + communityModule: root.communityModule visible: showContactList hideCommunityMembers: root.hideCommunityMembers anchors.topMargin: this.height > 0 ? Style.current.halfPadding : 0 diff --git a/ui/app/AppLayouts/Chat/popups/community/AccessExistingCommunityPopup.qml b/ui/imports/shared/popups/ImportCommunityPopup.qml similarity index 89% rename from ui/app/AppLayouts/Chat/popups/community/AccessExistingCommunityPopup.qml rename to ui/imports/shared/popups/ImportCommunityPopup.qml index 686b5d0478..f7a97659f7 100644 --- a/ui/app/AppLayouts/Chat/popups/community/AccessExistingCommunityPopup.qml +++ b/ui/imports/shared/popups/ImportCommunityPopup.qml @@ -13,7 +13,7 @@ import StatusQ.Controls 0.1 as StatusQControls StatusModal { id: root - width: 400 + width: 640 height: 400 property var store @@ -22,8 +22,8 @@ StatusModal { return Utils.isPrivateKey(communityKey) && Utils.startsWith0x(communityKey) } - //% "Access existing community" - header.title: qsTrId("access-existing-community") + //% "Import Community" + header.title: qsTrId("Import Community") onClosed: { root.destroy(); @@ -37,12 +37,26 @@ StatusModal { anchors.rightMargin: 16 height: childrenRect.height + StatusBaseText { + id: infoText1 + anchors.top: parent.top + anchors.topMargin: Style.current.padding + //% "Entering a community key will grant you the ownership of that community. Please be responsible with it and don’t share the key with people you don’t trust." + text: qsTrId("entering-a-community-key-will-grant-you-the-ownership-of-that-community--please-be-responsible-with-it-and-don-t-share-the-key-with-people-you-don-t-trust-") + wrapMode: Text.WordWrap + width: parent.width + font.pixelSize: 13 + color: Theme.palette.baseColor1 + } + StyledTextArea { id: keyInput //% "Community private key" - label: qsTrId("community-key") + label: qsTrId("Community key") placeholderText: "0x0..." customHeight: 110 + anchors.top: infoText1.bottom + anchors.topMargin: Style.current.bigPadding anchors.left: parent.left anchors.right: parent.right @@ -50,18 +64,6 @@ StatusModal { importButton.enabled = root.validate(keyInput.text) } } - - StatusBaseText { - id: infoText1 - //% "Entering a community key will grant you the ownership of that community. Please be responsible with it and don’t share the key with people you don’t trust." - text: qsTrId("entering-a-community-key-will-grant-you-the-ownership-of-that-community--please-be-responsible-with-it-and-don-t-share-the-key-with-people-you-don-t-trust-") - anchors.top: keyInput.bottom - wrapMode: Text.WordWrap - anchors.topMargin: Style.current.bigPadding - width: parent.width - font.pixelSize: 13 - color: Theme.palette.baseColor1 - } } rightButtons: [ diff --git a/ui/imports/shared/popups/qmldir b/ui/imports/shared/popups/qmldir index d52ada9d2f..aca329794f 100644 --- a/ui/imports/shared/popups/qmldir +++ b/ui/imports/shared/popups/qmldir @@ -15,4 +15,5 @@ UserStatusContextMenu 1.0 UserStatusContextMenu.qml SignTransactionModal 1.0 SignTransactionModal.qml SelectAccountModal 1.0 SelectAccountModal.qml ProfilePopup 1.0 ProfilePopup.qml -ImageCropWorkflow 1.0 ImageCropWorkflow.qml \ No newline at end of file +ImageCropWorkflow 1.0 ImageCropWorkflow.qml +ImportCommunityPopup 1.0 ImportCommunityPopup.qml diff --git a/ui/imports/shared/views/ExistingContacts.qml b/ui/imports/shared/views/ExistingContacts.qml index f1df3067fb..5e269b9c88 100644 --- a/ui/imports/shared/views/ExistingContacts.qml +++ b/ui/imports/shared/views/ExistingContacts.qml @@ -16,6 +16,7 @@ Item { property var contactsStore property var community + property var communityModule property string filterText: "" property bool expanded: true @@ -56,7 +57,7 @@ Item { model.name.toLowerCase().includes(root.filterText.toLowerCase()) || model.pubKey.toLowerCase().includes(root.filterText.toLowerCase())) && (!root.hideCommunityMembers || - !root.community.hasMember(model.pubKey)) + !root.communityModule.hasMember(commmunity.id, model.pubKey)) } onContactClicked: function () { root.contactClicked(model) diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index db8d759c6f..fd10ca720f 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -45,8 +45,9 @@ QtObject { property int browserSettings: 10 property int advanced: 11 property int about: 12 - property int signout: 13 - property int backUpSeed: 14 + property int communitiesSettings: 13 + property int signout: 14 + property int backUpSeed: 15 } readonly property QtObject userStatus: QtObject{