fix(@desktop/profile): profile social links

- added to sync mechanism
- added to backup mechanism
- UI updated according to the newest changes

Closes: #10390
This commit is contained in:
Sale Djenic 2023-06-06 13:59:50 +02:00 committed by saledjenic
parent 274fc98839
commit ae492fe631
20 changed files with 320 additions and 137 deletions

View File

@ -2,6 +2,7 @@ import json, chronicles
import base import base
import ../../../../app_service/common/social_links
import ../../../../app_service/service/message/dto/[message, pinned_message_update, reaction, removed_message] import ../../../../app_service/service/message/dto/[message, pinned_message_update, reaction, removed_message]
import ../../../../app_service/service/chat/dto/[chat] import ../../../../app_service/service/chat/dto/[chat]
import ../../../../app_service/service/bookmarks/dto/[bookmark] import ../../../../app_service/service/bookmarks/dto/[bookmark]
@ -30,6 +31,7 @@ type MessageSignal* = ref object of Signal
removedChats*: seq[string] removedChats*: seq[string]
currentStatus*: seq[StatusUpdateDto] currentStatus*: seq[StatusUpdateDto]
settings*: seq[SettingsFieldDto] settings*: seq[SettingsFieldDto]
socialLinksInfo*: SocialLinksInfo
clearedHistories*: seq[ClearedHistoryDto] clearedHistories*: seq[ClearedHistoryDto]
verificationRequests*: seq[VerificationRequest] verificationRequests*: seq[VerificationRequest]
savedAddresses*: seq[SavedAddressDto] savedAddresses*: seq[SavedAddressDto]
@ -53,102 +55,110 @@ proc fromEvent*(T: type MessageSignal, event: JsonNode): MessageSignal =
signal.messages = @[] signal.messages = @[]
signal.contacts = @[] signal.contacts = @[]
if event["event"]{"contacts"} != nil: if not event.contains("event"):
for jsonContact in event["event"]["contacts"]: return signal
let e = event["event"]
if e.contains("contacts"):
for jsonContact in e["contacts"]:
signal.contacts.add(jsonContact.toContactsDto()) signal.contacts.add(jsonContact.toContactsDto())
if event["event"]{"messages"} != nil: if e.contains("messages"):
for jsonMsg in event["event"]["messages"]: for jsonMsg in e["messages"]:
var message = jsonMsg.toMessageDto() var message = jsonMsg.toMessageDto()
signal.messages.add(message) signal.messages.add(message)
info "received", signal="messages.new", messageID=message.id info "received", signal="messages.new", messageID=message.id
if event["event"]{"chats"} != nil: if e.contains("chats"):
for jsonChat in event["event"]["chats"]: for jsonChat in e["chats"]:
var chat = jsonChat.toChatDto() var chat = jsonChat.toChatDto()
signal.chats.add(chat) signal.chats.add(chat)
if event["event"]{"clearedHistories"} != nil: if e.contains("clearedHistories"):
for jsonClearedHistory in event["event"]{"clearedHistories"}: for jsonClearedHistory in e["clearedHistories"]:
var clearedHistoryDto = jsonClearedHistory.toClearedHistoryDto() var clearedHistoryDto = jsonClearedHistory.toClearedHistoryDto()
signal.clearedHistories.add(clearedHistoryDto) signal.clearedHistories.add(clearedHistoryDto)
if event["event"]{"statusUpdates"} != nil: if e.contains("statusUpdates"):
for jsonStatusUpdate in event["event"]["statusUpdates"]: for jsonStatusUpdate in e["statusUpdates"]:
var statusUpdate = jsonStatusUpdate.toStatusUpdateDto() var statusUpdate = jsonStatusUpdate.toStatusUpdateDto()
signal.statusUpdates.add(statusUpdate) signal.statusUpdates.add(statusUpdate)
if event["event"]{"currentStatus"} != nil: if e.contains("currentStatus"):
var currentStatus = event["event"]["currentStatus"].toStatusUpdateDto() var currentStatus = e["currentStatus"].toStatusUpdateDto()
signal.currentStatus.add(currentStatus) signal.currentStatus.add(currentStatus)
if event["event"]{"bookmarks"} != nil: if e.contains("bookmarks"):
for jsonBookmark in event["event"]["bookmarks"]: for jsonBookmark in e["bookmarks"]:
var bookmark = jsonBookmark.toBookmarkDto() var bookmark = jsonBookmark.toBookmarkDto()
signal.bookmarks.add(bookmark) signal.bookmarks.add(bookmark)
if event["event"]{"installations"} != nil: if e.contains("installations"):
for jsonDevice in event["event"]["installations"]: for jsonDevice in e["installations"]:
signal.installations.add(jsonDevice.toInstallationDto()) signal.installations.add(jsonDevice.toInstallationDto())
if event["event"]{"emojiReactions"} != nil: if e.contains("emojiReactions"):
for jsonReaction in event["event"]["emojiReactions"]: for jsonReaction in e["emojiReactions"]:
signal.emojiReactions.add(jsonReaction.toReactionDto()) signal.emojiReactions.add(jsonReaction.toReactionDto())
if event["event"]{"communities"} != nil: if e.contains("communities"):
for jsonCommunity in event["event"]["communities"]: for jsonCommunity in e["communities"]:
signal.communities.add(jsonCommunity.toCommunityDto()) signal.communities.add(jsonCommunity.toCommunityDto())
if event["event"]{"communitiesSettings"} != nil: if e.contains("communitiesSettings"):
for jsonCommunitySettings in event["event"]["communitiesSettings"]: for jsonCommunitySettings in e["communitiesSettings"]:
signal.communitiesSettings.add(jsonCommunitySettings.toCommunitySettingsDto()) signal.communitiesSettings.add(jsonCommunitySettings.toCommunitySettingsDto())
if event["event"]{"requestsToJoinCommunity"} != nil: if e.contains("requestsToJoinCommunity"):
for jsonCommunity in event["event"]["requestsToJoinCommunity"]: for jsonCommunity in e["requestsToJoinCommunity"]:
signal.membershipRequests.add(jsonCommunity.toCommunityMembershipRequestDto()) signal.membershipRequests.add(jsonCommunity.toCommunityMembershipRequestDto())
if event["event"]{"removedMessages"} != nil: if e.contains("removedMessages"):
for jsonRemovedMessage in event["event"]["removedMessages"]: for jsonRemovedMessage in e["removedMessages"]:
signal.deletedMessages.add(jsonRemovedMessage.toRemovedMessageDto()) signal.deletedMessages.add(jsonRemovedMessage.toRemovedMessageDto())
if event["event"]{"removedChats"} != nil: if e.contains("removedChats"):
for removedChatID in event["event"]["removedChats"]: for removedChatID in e["removedChats"]:
signal.removedChats.add(removedChatID.getStr()) signal.removedChats.add(removedChatID.getStr())
if event["event"]{"activityCenterNotifications"} != nil: if e.contains("activityCenterNotifications"):
for jsonNotification in event["event"]["activityCenterNotifications"]: for jsonNotification in e["activityCenterNotifications"]:
signal.activityCenterNotifications.add(jsonNotification.toActivityCenterNotificationDto()) signal.activityCenterNotifications.add(jsonNotification.toActivityCenterNotificationDto())
if event["event"]{"pinMessages"} != nil: if e.contains("pinMessages"):
for jsonPinnedMessage in event["event"]["pinMessages"]: for jsonPinnedMessage in e["pinMessages"]:
signal.pinnedMessages.add(jsonPinnedMessage.toPinnedMessageUpdateDto()) signal.pinnedMessages.add(jsonPinnedMessage.toPinnedMessageUpdateDto())
if event["event"]{"settings"} != nil: if e.contains("settings"):
for jsonSettingsField in event["event"]["settings"]: for jsonSettingsField in e["settings"]:
signal.settings.add(jsonSettingsField.toSettingsFieldDto()) signal.settings.add(jsonSettingsField.toSettingsFieldDto())
if event["event"]{"verificationRequests"} != nil: if e.contains("socialLinksInfo"):
for jsonVerificationRequest in event["event"]["verificationRequests"]: signal.socialLinksInfo = toSocialLinksInfo(e["socialLinksInfo"])
if e.contains("verificationRequests"):
for jsonVerificationRequest in e["verificationRequests"]:
signal.verificationRequests.add(jsonVerificationRequest.toVerificationRequest()) signal.verificationRequests.add(jsonVerificationRequest.toVerificationRequest())
if event["event"]{"savedAddresses"} != nil: if e.contains("savedAddresses"):
for jsonSavedAddress in event["event"]["savedAddresses"]: for jsonSavedAddress in e["savedAddresses"]:
signal.savedAddresses.add(jsonSavedAddress.toSavedAddressDto()) signal.savedAddresses.add(jsonSavedAddress.toSavedAddressDto())
if event["event"]{"keypairs"} != nil: if e.contains("keypairs"):
for jsonKc in event["event"]["keypairs"]: for jsonKc in e["keypairs"]:
signal.keypairs.add(jsonKc.toKeypairDto()) signal.keypairs.add(jsonKc.toKeypairDto())
if event["event"]{"keycards"} != nil: if e.contains("keycards"):
for jsonKc in event["event"]["keycards"]: for jsonKc in e["keycards"]:
signal.keycards.add(jsonKc.toKeycardDto()) signal.keycards.add(jsonKc.toKeycardDto())
if event["event"]{"keycardActions"} != nil: if e.contains("keycardActions"):
for jsonKc in event["event"]["keycardActions"]: for jsonKc in e["keycardActions"]:
signal.keycardActions.add(jsonKc.toKeycardActionDto()) signal.keycardActions.add(jsonKc.toKeycardActionDto())
if event["event"]{"accounts"} != nil: if e.contains("accounts"):
for jsonAcc in event["event"]["accounts"]: for jsonAcc in e["accounts"]:
signal.walletAccounts.add(jsonAcc.toWalletAccountDto()) signal.walletAccounts.add(jsonAcc.toWalletAccountDto())
result = signal result = signal

View File

@ -24,10 +24,16 @@ proc delete*(self: Controller) =
discard discard
proc init*(self: Controller) = proc init*(self: Controller) =
self.settingsService.fetchAndStoreSocialLinks()
self.events.on(SIGNAL_BIO_UPDATED) do(e: Args): self.events.on(SIGNAL_BIO_UPDATED) do(e: Args):
let args = SettingsTextValueArgs(e) let args = SettingsTextValueArgs(e)
self.delegate.onBioChanged(args.value) self.delegate.onBioChanged(args.value)
self.events.on(SIGNAL_SOCIAL_LINKS_UPDATED) do(e: Args):
let args = SocialLinksArgs(e)
self.delegate.onSocialLinksUpdated(args.socialLinks, args.error)
proc storeIdentityImage*(self: Controller, address: string, image: string, aX: int, aY: int, bX: int, bY: int) = proc storeIdentityImage*(self: Controller, address: string, image: string, aX: int, aY: int, bX: int, bY: int) =
discard self.profileService.storeIdentityImage(address, image, aX, aY, bX, bY) discard self.profileService.storeIdentityImage(address, image, aX, aY, bX, bY)
@ -38,9 +44,9 @@ proc setDisplayName*(self: Controller, displayName: string) =
self.profileService.setDisplayName(displayName) self.profileService.setDisplayName(displayName)
proc getSocialLinks*(self: Controller): SocialLinks = proc getSocialLinks*(self: Controller): SocialLinks =
self.settingsService.getSocialLinks() return self.settingsService.getSocialLinks()
proc setSocialLinks*(self: Controller, links: SocialLinks): bool = proc setSocialLinks*(self: Controller, links: SocialLinks) =
self.settingsService.setSocialLinks(links) self.settingsService.setSocialLinks(links)
proc getBio*(self: Controller): string = proc getBio*(self: Controller): string =

View File

@ -1,4 +1,5 @@
import NimQml import NimQml
import ../../../../../app_service/common/social_links
type type
AccessInterface* {.pure inheritable.} = ref object of RootObj AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -34,7 +35,10 @@ method onBioChanged*(self: AccessInterface, bio: string) {.base.} =
method setDisplayName*(self: AccessInterface, displayName: string) {.base.} = method setDisplayName*(self: AccessInterface, displayName: string) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method saveSocialLinks*(self: AccessInterface): bool {.base.} = method saveSocialLinks*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method onSocialLinksUpdated*(self: AccessInterface, socialLinks: SocialLinks, error: string) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
# View Delegate Interface # View Delegate Interface

View File

@ -49,12 +49,12 @@ method isLoaded*(self: Module): bool =
method getModuleAsVariant*(self: Module): QVariant = method getModuleAsVariant*(self: Module): QVariant =
return self.viewVariant return self.viewVariant
proc updateSocialLinks(self: Module, socialLinks: SocialLinks) =
var socialLinkItems = toSocialLinkItems(socialLinks)
self.view.socialLinksSaved(socialLinkItems)
method viewDidLoad*(self: Module) = method viewDidLoad*(self: Module) =
var socialLinkItems = toSocialLinkItems(self.controller.getSocialLinks()) self.updateSocialLinks(self.controller.getSocialLinks())
self.view.socialLinksModel().setItems(socialLinkItems)
self.view.temporarySocialLinksModel().setItems(socialLinkItems)
self.moduleLoaded = true self.moduleLoaded = true
self.delegate.profileModuleDidLoad() self.delegate.profileModuleDidLoad()
@ -79,6 +79,12 @@ method setBio(self: Module, bio: string) =
method onBioChanged*(self: Module, bio: string) = method onBioChanged*(self: Module, bio: string) =
self.view.emitBioChangedSignal() self.view.emitBioChangedSignal()
method saveSocialLinks*(self: Module): bool = method saveSocialLinks*(self: Module) =
let socialLinks = map(self.view.temporarySocialLinksModel.items(), x => SocialLink(text: x.text, url: x.url)) let socialLinks = map(self.view.temporarySocialLinksModel.items(), x => SocialLink(text: x.text, url: x.url, icon: x.icon))
return self.controller.setSocialLinks(socialLinks) self.controller.setSocialLinks(socialLinks)
method onSocialLinksUpdated*(self: Module, socialLinks: SocialLinks, error: string) =
if error.len > 0:
# maybe we want in future popup or somehow display an error to a user
return
self.updateSocialLinks(socialLinks)

View File

@ -90,6 +90,9 @@ QtObject:
read = areSocialLinksDirty read = areSocialLinksDirty
notify = socialLinksDirtyChanged notify = socialLinksDirtyChanged
proc containsSocialLink*(self: View, text: string, url: string): bool {.slot.} =
return self.temporarySocialLinksModel.containsSocialLink(text, url)
proc createLink(self: View, text: string, url: string, linkType: int, icon: string) {.slot.} = proc createLink(self: View, text: string, url: string, linkType: int, icon: string) {.slot.} =
self.temporarySocialLinksModel.appendItem(initSocialLinkItem(text, url, (LinkType)linkType, icon)) self.temporarySocialLinksModel.appendItem(initSocialLinkItem(text, url, (LinkType)linkType, icon))
self.temporarySocialLinksJsonChanged() self.temporarySocialLinksJsonChanged()
@ -114,14 +117,16 @@ QtObject:
self.socialLinksDirtyChanged() self.socialLinksDirtyChanged()
self.temporarySocialLinksJsonChanged() self.temporarySocialLinksJsonChanged()
proc saveSocialLinks(self: View, silent: bool = false): bool {.slot.} = proc socialLinksSaved*(self: View, items: seq[SocialLinkItem]) =
result = self.delegate.saveSocialLinks() self.socialLinksModel.setItems(items)
if (result): self.temporarySocialLinksModel.setItems(items)
self.socialLinksModel.setItems(self.temporarySocialLinksModel.items) self.socialLinksJsonChanged()
self.socialLinksJsonChanged() self.temporarySocialLinksJsonChanged()
self.temporarySocialLinksJsonChanged()
if not silent: proc saveSocialLinks(self: View, silent: bool = false) {.slot.} =
self.socialLinksDirtyChanged() self.delegate.saveSocialLinks()
if not silent:
self.socialLinksDirtyChanged()
proc bioChanged*(self: View) {.signal.} proc bioChanged*(self: View) {.signal.}
proc getBio(self: View): string {.slot.} = proc getBio(self: View): string {.slot.} =

View File

@ -1,4 +1,4 @@
import NimQml, tables, sequtils, sugar import NimQml, tables, strutils, sequtils, sugar
import ../../../app_service/common/social_links import ../../../app_service/common/social_links
@ -39,10 +39,21 @@ QtObject:
new(result, delete) new(result, delete)
result.setup result.setup
proc countChanged(self: SocialLinksModel) {.signal.}
proc getCount(self: SocialLinksModel): int {.slot.} =
self.items.len
QtProperty[int] count:
read = getCount
notify = countChanged
proc containsSocialLink*(self: SocialLinksModel, text, url: string): bool =
return self.items.any(item => cmpIgnoreCase(item.text, text) == 0 and cmpIgnoreCase(item.url, url) == 0)
proc setItems*(self: SocialLinksModel, items: seq[SocialLinkItem]) = proc setItems*(self: SocialLinksModel, items: seq[SocialLinkItem]) =
self.beginResetModel() self.beginResetModel()
self.items = items self.items = items
self.endResetModel() self.endResetModel()
self.countChanged()
proc appendItem*(self: SocialLinksModel, item: SocialLinkItem) = proc appendItem*(self: SocialLinksModel, item: SocialLinkItem) =
let parentModelIndex = newQModelIndex() let parentModelIndex = newQModelIndex()
@ -50,6 +61,7 @@ QtObject:
self.beginInsertRows(parentModelIndex, self.items.len, self.items.len) self.beginInsertRows(parentModelIndex, self.items.len, self.items.len)
self.items.add(item) self.items.add(item)
self.endInsertRows() self.endInsertRows()
self.countChanged()
proc removeItem*(self: SocialLinksModel, uuid: string): bool = proc removeItem*(self: SocialLinksModel, uuid: string): bool =
for i in 0 ..< self.items.len: for i in 0 ..< self.items.len:
@ -59,6 +71,7 @@ QtObject:
self.beginRemoveRows(parentModelIndex, i, i) self.beginRemoveRows(parentModelIndex, i, i)
self.items.delete(i) self.items.delete(i)
self.endRemoveRows() self.endRemoveRows()
self.countChanged()
return true return true
return false return false

View File

@ -336,7 +336,6 @@ method checkFetchingStatusAndProceedWithAppLoading*[T](self: Module[T]) =
method onFetchingFromWakuMessageReceived*[T](self: Module[T], backedUpMsgClock: uint64, section: string, method onFetchingFromWakuMessageReceived*[T](self: Module[T], backedUpMsgClock: uint64, section: string,
totalMessages: int, receivedMessageAtPosition: int) = totalMessages: int, receivedMessageAtPosition: int) =
echo "onFetchingFromWakuMessageReceived: ", backedUpMsgClock, " section: ", section, " tm: ", totalMessages, " recAtPos: ", receivedMessageAtPosition
self.view.fetchingDataModel().checkLastKnownClockAndReinitModel(backedUpMsgClock, listOfEntitiesWeExpectToBeSynced) self.view.fetchingDataModel().checkLastKnownClockAndReinitModel(backedUpMsgClock, listOfEntitiesWeExpectToBeSynced)
if self.view.fetchingDataModel().allMessagesLoaded(): if self.view.fetchingDataModel().allMessagesLoaded():
return return

View File

@ -1,5 +1,7 @@
import json, sequtils, sugar import json, sequtils, sugar
include json_utils
const SOCIAL_LINK_TWITTER_ID* = "__twitter" const SOCIAL_LINK_TWITTER_ID* = "__twitter"
const SOCIAL_LINK_PERSONAL_SITE_ID* = "__personal_site" const SOCIAL_LINK_PERSONAL_SITE_ID* = "__personal_site"
const SOCIAL_LINK_GITHUB_ID* = "__github" const SOCIAL_LINK_GITHUB_ID* = "__github"
@ -15,6 +17,10 @@ type
SocialLinks* = seq[SocialLink] SocialLinks* = seq[SocialLink]
SocialLinksInfo* = object
links*: seq[SocialLink]
removed*: bool
proc socialLinkTextToIcon(text: string): string = proc socialLinkTextToIcon(text: string): string =
if (text == SOCIAL_LINK_TWITTER_ID): return "twitter" if (text == SOCIAL_LINK_TWITTER_ID): return "twitter"
if (text == SOCIAL_LINK_PERSONAL_SITE_ID): return "language" if (text == SOCIAL_LINK_PERSONAL_SITE_ID): return "language"
@ -30,7 +36,12 @@ proc toSocialLinks*(jsonObj: JsonNode): SocialLinks =
url: node["url"].getStr(), url: node["url"].getStr(),
icon: socialLinkTextToIcon(node["text"].getStr())) icon: socialLinkTextToIcon(node["text"].getStr()))
) )
return
proc toSocialLinksInfo*(jsonObj: JsonNode): SocialLinksInfo =
discard jsonObj.getProp("removed", result.removed)
var linksObj: JsonNode
if jsonObj.getProp("links", linksObj):
result.links = toSocialLinks(linksObj)
proc toJsonNode*(links: SocialLinks): JsonNode = proc toJsonNode*(links: SocialLinks): JsonNode =
%*links %*links

View File

@ -4,6 +4,7 @@ import json
import ../../visual_identity/dto import ../../visual_identity/dto
include ../../../common/json_utils include ../../../common/json_utils
import ../../../common/social_links
type type
Image* = object Image* = object
@ -28,6 +29,7 @@ type AccountDto* = object
type WakuBackedUpProfileDto* = object type WakuBackedUpProfileDto* = object
displayName*: string displayName*: string
images*: seq[Image] images*: seq[Image]
socialLinks*: SocialLinks
proc isValid*(self: AccountDto): bool = proc isValid*(self: AccountDto): bool =
result = self.name.len > 0 and self.keyUid.len > 0 result = self.name.len > 0 and self.keyUid.len > 0
@ -69,7 +71,10 @@ proc toWakuBackedUpProfileDto*(jsonObj: JsonNode): WakuBackedUpProfileDto =
result = WakuBackedUpProfileDto() result = WakuBackedUpProfileDto()
discard jsonObj.getProp("displayName", result.displayName) discard jsonObj.getProp("displayName", result.displayName)
var imagesObj: JsonNode var obj: JsonNode
if(jsonObj.getProp("images", imagesObj) and imagesObj.kind == JArray): if(jsonObj.getProp("images", obj) and obj.kind == JArray):
for imgObj in imagesObj: for imgObj in obj:
result.images.add(toImage(imgObj)) result.images.add(toImage(imgObj))
if(jsonObj.getProp("socialLinks", obj) and obj.kind == JArray):
result.socialLinks = toSocialLinks(obj)

View File

@ -25,6 +25,7 @@ const SIGNAL_CURRENCY_UPDATED* = "currencyUpdated"
const SIGNAL_DISPLAY_NAME_UPDATED* = "displayNameUpdated" const SIGNAL_DISPLAY_NAME_UPDATED* = "displayNameUpdated"
const SIGNAL_BIO_UPDATED* = "bioUpdated" const SIGNAL_BIO_UPDATED* = "bioUpdated"
const SIGNAL_MNEMONIC_REMOVED* = "mnemonicRemoved" const SIGNAL_MNEMONIC_REMOVED* = "mnemonicRemoved"
const SIGNAL_SOCIAL_LINKS_UPDATED* = "socialLinksUpdated"
const SIGNAL_CURRENT_USER_STATUS_UPDATED* = "currentUserStatusUpdated" const SIGNAL_CURRENT_USER_STATUS_UPDATED* = "currentUserStatusUpdated"
logScope: logScope:
@ -38,6 +39,13 @@ type
statusType*: StatusType statusType*: StatusType
text*: string text*: string
SocialLinksArgs* = ref object of Args
socialLinks*: SocialLinks
error*: string
SettingProfilePictureArgs* = ref object of Args
value*: int
QtObject: QtObject:
type Service* = ref object of QObject type Service* = ref object of QObject
events: EventEmitter events: EventEmitter
@ -47,7 +55,7 @@ QtObject:
notifExemptionsCache: Table[string, NotificationsExemptions] notifExemptionsCache: Table[string, NotificationsExemptions]
# Forward declaration # Forward declaration
proc fetchSocialLinks*(self: Service): SocialLinks proc storeSocialLinksAndNotify(self: Service, data: SocialLinksArgs)
proc initNotificationSettings*(self: Service) proc initNotificationSettings*(self: Service)
proc getNotifSettingAllowNotifications*(self: Service): bool proc getNotifSettingAllowNotifications*(self: Service): bool
proc getNotifSettingOneToOneChats*(self: Service): string proc getNotifSettingOneToOneChats*(self: Service): string
@ -76,7 +84,6 @@ QtObject:
let response = status_settings.getSettings() let response = status_settings.getSettings()
self.settings = response.result.toSettingsDto() self.settings = response.result.toSettingsDto()
self.initNotificationSettings() self.initNotificationSettings()
self.socialLinks = self.fetchSocialLinks()
except Exception as e: except Exception as e:
let errDesription = e.msg let errDesription = e.msg
error "error: ", errDesription error "error: ", errDesription
@ -103,6 +110,10 @@ QtObject:
self.settings.mnemonic = "" self.settings.mnemonic = ""
self.events.emit(SIGNAL_MNEMONIC_REMOVED, Args()) self.events.emit(SIGNAL_MNEMONIC_REMOVED, Args())
if receivedData.socialLinksInfo.links.len > 0 or
receivedData.socialLinksInfo.removed:
self.storeSocialLinksAndNotify(SocialLinksArgs(socialLinks: receivedData.socialLinksInfo.links))
self.initialized = true self.initialized = true
proc initNotificationSettings(self: Service) = proc initNotificationSettings(self: Service) =
@ -920,32 +931,38 @@ QtObject:
proc getSocialLinks*(self: Service): SocialLinks = proc getSocialLinks*(self: Service): SocialLinks =
return self.socialLinks return self.socialLinks
proc fetchSocialLinks*(self: Service): SocialLinks = proc storeSocialLinksAndNotify(self: Service, data: SocialLinksArgs) =
self.socialLinks = data.socialLinks
self.events.emit(SIGNAL_SOCIAL_LINKS_UPDATED, data)
proc fetchAndStoreSocialLinks*(self: Service) =
var data = SocialLinksArgs()
try: try:
let response = status_settings.getSocialLinks() let response = status_settings.getSocialLinks()
if(not response.error.isNil): if(not response.error.isNil):
data.error = response.error.message
error "error getting social links", errDescription = response.error.message error "error getting social links", errDescription = response.error.message
data.socialLinks = toSocialLinks(response.result)
result = toSocialLinks(response.result)
except Exception as e: except Exception as e:
data.error = e.msg
error "error getting social links", errDesription = e.msg error "error getting social links", errDesription = e.msg
self.storeSocialLinksAndNotify(data)
proc setSocialLinks*(self: Service, links: SocialLinks): bool = proc setSocialLinks*(self: Service, links: SocialLinks) =
result = false var data = SocialLinksArgs()
let isValid = all(links, proc (link: SocialLink): bool = common_utils.validateLink(link.url)) let isValid = all(links, proc (link: SocialLink): bool = common_utils.validateLink(link.url))
if (not isValid): if not isValid:
error "error saving social links" data.error = "invalid link provided"
return result error "validation error", errDescription=data.error
return
try: try:
let response = status_settings.setSocialLinks(%*links) let response = status_settings.addOrReplaceSocialLinks(%*links)
if not response.error.isNil:
if(not response.error.isNil): data.error = response.error.message
error "error setting social links", errDescription = response.error.message error "error saving social links", errDescription=data.error
return
self.socialLinks = self.fetchSocialLinks() data.socialLinks = links
result = true
except Exception as e: except Exception as e:
error "error setting social links", errDesription = e.msg data.error = e.msg
error "error saving social links", errDescription=data.error
self.storeSocialLinksAndNotify(data)

View File

@ -96,8 +96,8 @@ proc setExemptions*(id: string, muteAllMessages: bool, personalMentions: string,
proc deleteExemptions*(id: string): RpcResponse[JsonNode] {.raises: [Exception].} = proc deleteExemptions*(id: string): RpcResponse[JsonNode] {.raises: [Exception].} =
return core.callPrivateRPC("settings_deleteExemptions", %* [id]) return core.callPrivateRPC("settings_deleteExemptions", %* [id])
proc setSocialLinks*(value: JsonNode): RpcResponse[JsonNode] {.raises: [Exception].} = proc addOrReplaceSocialLinks*(value: JsonNode): RpcResponse[JsonNode] {.raises: [Exception].} =
return core.callPrivateRPC("settings_setSocialLinks", %* [value]) return core.callPrivateRPC("settings_addOrReplaceSocialLinks", %* [value])
proc getSocialLinks*(): RpcResponse[JsonNode] {.raises: [Exception].} = proc getSocialLinks*(): RpcResponse[JsonNode] {.raises: [Exception].} =
return core.callPrivateRPC("settings_getSocialLinks") return core.callPrivateRPC("settings_getSocialLinks")

View File

@ -440,11 +440,11 @@ class ProfileSettingsView(BaseElement):
verify_equals(8, len(table)) # Expecting 8 as social media link fields to verify verify_equals(8, len(table)) # Expecting 8 as social media link fields to verify
links = { links = {
'Twitter': [table[0][0]], 'Twitter': [table[0][0]],
'Personal Site': [table[1][0]], 'Personal site': [table[1][0]],
'Github': [table[2][0]], 'Github': [table[2][0]],
'YouTube': [table[3][0]], 'YouTube channel': [table[3][0]],
'Discord': [table[4][0]], 'Discord handle': [table[4][0]],
'Telegram': [table[5][0]], 'Telegram handle': [table[5][0]],
'Custom link': [table[6][0], table[7][0]], 'Custom link': [table[6][0], table[7][0]],
} }
@ -481,11 +481,11 @@ class ProfileSettingsView(BaseElement):
links = self.social_links links = self.social_links
compare_text(links['Twitter'], twitter) compare_text(links['Twitter'], twitter)
compare_text(links['Personal Site'], personal_site) compare_text(links['Personal site'], personal_site)
compare_text(links['Github'], github) compare_text(links['Github'], github)
compare_text(links['YouTube'], youtube) compare_text(links['YouTube channel'], youtube)
compare_text(links['Discord'], discord) compare_text(links['Discord handle'], discord)
compare_text(links['Telegram'], telegram) compare_text(links['Telegram handle'], telegram)
compare_text(links[custom_link_text], custom_link) compare_text(links[custom_link_text], custom_link)
def verify_social_no_links(self): def verify_social_no_links(self):

View File

@ -26,6 +26,7 @@ Control {
Component { Component {
id: addSocialLinkModalComponent id: addSocialLinkModalComponent
AddSocialLinkModal { AddSocialLinkModal {
containsSocialLink: root.profileStore.containsSocialLink
onAddLinkRequested: root.profileStore.createLink(linkText, linkUrl, linkType, linkIcon) onAddLinkRequested: root.profileStore.createLink(linkText, linkUrl, linkType, linkIcon)
} }
} }
@ -33,6 +34,7 @@ Control {
Component { Component {
id: modifySocialLinkModal id: modifySocialLinkModal
ModifySocialLinkModal { ModifySocialLinkModal {
containsSocialLink: root.profileStore.containsSocialLink
onUpdateLinkRequested: root.profileStore.updateLink(uuid, linkText, linkUrl) onUpdateLinkRequested: root.profileStore.updateLink(uuid, linkText, linkUrl)
onRemoveLinkRequested: root.profileStore.removeLink(uuid) onRemoveLinkRequested: root.profileStore.removeLink(uuid)
} }
@ -50,31 +52,31 @@ Control {
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
StatusLinkText { StatusBaseText {
objectName: "addMoreSocialLinks" text: qsTr("%1/%2").arg(root.profileStore.temporarySocialLinksModel.count).arg(Constants.maxNumOfSocialLinks)
text: qsTr(" Add more links") color: Theme.palette.baseColor1
color: Theme.palette.primaryColor1
font.pixelSize: Theme.tertiaryTextFontSize
font.weight: Font.Normal
onClicked: Global.openPopup(addSocialLinkModalComponent)
}
}
SortFilterProxyModel {
id: filteredSocialLinksModel
sourceModel: root.socialLinksModel
filters: ExpressionFilter {
expression: model.text !== "" && model.url !== ""
} }
} }
// empty placeholder when no links; dashed rounded rectangle // empty placeholder when no links; dashed rounded rectangle
ShapeRectangle { ShapeRectangle {
readonly property bool maxReached: root.profileStore.temporarySocialLinksModel.count === Constants.maxNumOfSocialLinks
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width - 4 // the rectangular path is rendered outside Layout.preferredWidth: parent.width - 4 // the rectangular path is rendered outside
Layout.preferredHeight: 44 Layout.preferredHeight: 44
visible: !filteredSocialLinksModel.count text: maxReached? qsTr("Link limit of %1 reached").arg(Constants.maxNumOfSocialLinks) : ""
text: qsTr("Your links will appear here")
StatusLinkText {
objectName: "addMoreSocialLinks"
anchors.centerIn: parent
visible: !parent.maxReached
text: qsTr(" Add a link")
color: Theme.palette.primaryColor1
font.pixelSize: Theme.tertiaryTextFontSize
font.weight: Font.Normal
onClicked: Global.openPopup(addSocialLinkModalComponent)
}
} }
StatusListView { StatusListView {

View File

@ -7,6 +7,7 @@ import utils 1.0
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Popups 0.1 import StatusQ.Popups 0.1
@ -15,6 +16,7 @@ import AppLayouts.Profile.controls 1.0
StatusStackModal { StatusStackModal {
id: root id: root
property var containsSocialLink: function (text, url) {return false}
signal addLinkRequested(string linkText, string linkUrl, int linkType, string linkIcon) signal addLinkRequested(string linkText, string linkUrl, int linkType, string linkIcon)
implicitWidth: 480 // design implicitWidth: 480 // design
@ -26,7 +28,7 @@ StatusStackModal {
rightButtons: [finishButton] rightButtons: [finishButton]
finishButton: StatusButton { finishButton: StatusButton {
text: qsTr("Add") text: qsTr("Add")
enabled: !!linkTarget.text enabled: linkTarget.valid && (!customTitle.visible || customTitle.valid)
onClicked: { onClicked: {
root.addLinkRequested(d.selectedLinkTypeText || customTitle.text, // text for custom link, otherwise the link typeId root.addLinkRequested(d.selectedLinkTypeText || customTitle.text, // text for custom link, otherwise the link typeId
ProfileUtils.addSocialLinkPrefix(linkTarget.text, d.selectedLinkType), ProfileUtils.addSocialLinkPrefix(linkTarget.text, d.selectedLinkType),
@ -84,6 +86,8 @@ StatusStackModal {
asset.name: model.icon asset.name: model.icon
asset.color: ProfileUtils.linkTypeColor(model.type) asset.color: ProfileUtils.linkTypeColor(model.type)
onClicked: { onClicked: {
customTitle.reset()
linkTarget.reset()
d.selectedLinkIndex = index d.selectedLinkIndex = index
root.currentIndex++ root.currentIndex++
} }
@ -110,6 +114,28 @@ StatusStackModal {
icon: "language" icon: "language"
charLimit: Constants.maxSocialLinkTextLength charLimit: Constants.maxSocialLinkTextLength
input.tabNavItem: linkTarget.input.edit input.tabNavItem: linkTarget.input.edit
validators: [
StatusValidator {
name: "text-validation"
validate: (value) => {
return value.trim() !== ""
}
errorMessage: qsTr("Invalid title")
},
StatusValidator {
name: "check-social-link-existence"
validate: (value) => {
return !root.containsSocialLink(value,
ProfileUtils.addSocialLinkPrefix(linkTarget.text, d.selectedLinkType))
}
errorMessage: d.selectedLinkType === Constants.socialLinkType.custom?
qsTr("Name and link combination already added") :
qsTr("Link already added")
}
]
onValidChanged: {linkTarget.validate(true)}
onTextChanged: {linkTarget.validate(true)}
} }
StaticSocialLinkInput { StaticSocialLinkInput {
@ -121,6 +147,29 @@ StatusStackModal {
linkType: d.selectedLinkType linkType: d.selectedLinkType
icon: d.selectedIcon icon: d.selectedIcon
input.tabNavItem: customTitle.input.edit input.tabNavItem: customTitle.input.edit
validators: [
StatusValidator {
name: "link-validation"
validate: (value) => {
return value.trim() !== "" && Utils.validLink(ProfileUtils.addSocialLinkPrefix(value, d.selectedLinkType))
}
errorMessage: qsTr("Invalid %1").arg(ProfileUtils.linkTypeToDescription(linkTarget.linkType).toLowerCase() || qsTr("link"))
},
StatusValidator {
name: "check-social-link-existence"
validate: (value) => {
return !root.containsSocialLink(d.selectedLinkTypeText || customTitle.text,
ProfileUtils.addSocialLinkPrefix(value, d.selectedLinkType))
}
errorMessage: d.selectedLinkType === Constants.socialLinkType.custom?
qsTr("Name and link combination already added") :
qsTr("Link already added")
}
]
onValidChanged: {customTitle.validate(true)}
onTextChanged: {customTitle.validate(true)}
} }
} }
] ]

View File

@ -8,6 +8,7 @@ import utils 1.0
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Popups.Dialog 0.1 import StatusQ.Popups.Dialog 0.1
@ -16,6 +17,7 @@ import AppLayouts.Profile.controls 1.0
StatusDialog { StatusDialog {
id: root id: root
property var containsSocialLink: function (text, url) {return false}
property int linkType: -1 property int linkType: -1
property string icon property string icon
@ -44,12 +46,7 @@ StatusDialog {
rightButtons: ObjectModel { rightButtons: ObjectModel {
StatusButton { StatusButton {
text: qsTr("Update") text: qsTr("Update")
enabled: { enabled: linkTarget.valid && (!customTitle.visible || customTitle.valid)
if (!customTitle.visible)
return linkTarget.text && linkTarget.text !== linkUrl
return linkTarget.text && (linkTarget.text !== linkUrl || (customTitle.text && customTitle.text !== root.linkText))
}
onClicked: { onClicked: {
root.updateLinkRequested(root.uuid, customTitle.text, ProfileUtils.addSocialLinkPrefix(linkTarget.text, root.linkType)) root.updateLinkRequested(root.uuid, customTitle.text, ProfileUtils.addSocialLinkPrefix(linkTarget.text, root.linkType))
root.close() root.close()
@ -82,6 +79,29 @@ StatusDialog {
text: root.linkText text: root.linkText
charLimit: Constants.maxSocialLinkTextLength charLimit: Constants.maxSocialLinkTextLength
input.tabNavItem: linkTarget.input.edit input.tabNavItem: linkTarget.input.edit
validators: [
StatusValidator {
name: "text-validation"
validate: (value) => {
return value.trim() !== ""
}
errorMessage: qsTr("Invalid title")
},
StatusValidator {
name: "check-social-link-existence"
validate: (value) => {
return !root.containsSocialLink(value,
ProfileUtils.addSocialLinkPrefix(linkTarget.text, root.linkType))
}
errorMessage: root.linkType === Constants.socialLinkType.custom?
qsTr("Name and link combination already added") :
qsTr("Link already added")
}
]
onValidChanged: {linkTarget.validate(true)}
onTextChanged: {linkTarget.validate(true)}
} }
StaticSocialLinkInput { StaticSocialLinkInput {
@ -94,6 +114,29 @@ StatusDialog {
icon: root.icon icon: root.icon
text: root.linkUrl text: root.linkUrl
input.tabNavItem: customTitle.input.edit input.tabNavItem: customTitle.input.edit
validators: [
StatusValidator {
name: "link-validation"
validate: (value) => {
return value.trim() !== "" && Utils.validLink(ProfileUtils.addSocialLinkPrefix(value, root.linkType))
}
errorMessage: qsTr("Invalid %1").arg(ProfileUtils.linkTypeToDescription(linkTarget.linkType).toLowerCase() || qsTr("link"))
},
StatusValidator {
name: "check-social-link-existence"
validate: (value) => {
return !root.containsSocialLink(customTitle.text,
ProfileUtils.addSocialLinkPrefix(value, root.linkType))
}
errorMessage: root.linkType === Constants.socialLinkType.custom?
qsTr("Name and link combination already added") :
qsTr("Link already added")
}
]
onValidChanged: {customTitle.validate(true)}
onTextChanged: {customTitle.validate(true)}
} }
} }
} }

View File

@ -55,6 +55,10 @@ QtObject {
root.profileModule.setDisplayName(displayName) root.profileModule.setDisplayName(displayName)
} }
function containsSocialLink(text, url) {
return root.profileModule.containsSocialLink(text, url)
}
function createLink(text, url, linkType, icon) { function createLink(text, url, linkType, icon) {
root.profileModule.createLink(text, url, linkType, icon) root.profileModule.createLink(text, url, linkType, icon)
} }

View File

@ -617,6 +617,7 @@ QtObject {
readonly property int telegram: 6 readonly property int telegram: 6
} }
readonly property int maxNumOfSocialLinks: 20
readonly property int maxSocialLinkTextLength: 24 readonly property int maxSocialLinkTextLength: 24
readonly property QtObject localPairingEventType: QtObject { readonly property QtObject localPairingEventType: QtObject {

View File

@ -24,11 +24,11 @@ QtObject {
function linkTypeToText(linkType) { function linkTypeToText(linkType) {
if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter") if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter")
if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal Site") if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal site")
if (linkType === Constants.socialLinkType.github) return qsTr("Github") if (linkType === Constants.socialLinkType.github) return qsTr("Github")
if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube") if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube channel")
if (linkType === Constants.socialLinkType.discord) return qsTr("Discord") if (linkType === Constants.socialLinkType.discord) return qsTr("Discord handle")
if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram") if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram handle")
return "" // "custom" link type allows for user defined text return "" // "custom" link type allows for user defined text
} }
@ -42,12 +42,12 @@ QtObject {
} }
function linkTypeToDescription(linkType) { function linkTypeToDescription(linkType) {
if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter Handle") if (linkType === Constants.socialLinkType.twitter) return qsTr("Twitter username")
if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal Site") if (linkType === Constants.socialLinkType.personalSite) return qsTr("Personal site")
if (linkType === Constants.socialLinkType.github) return qsTr("Github") if (linkType === Constants.socialLinkType.github) return qsTr("Github")
if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube Channel") if (linkType === Constants.socialLinkType.youtube) return qsTr("YouTube channel")
if (linkType === Constants.socialLinkType.discord) return qsTr("Discord Handle") if (linkType === Constants.socialLinkType.discord) return qsTr("Discord handle")
if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram Handle") if (linkType === Constants.socialLinkType.telegram) return qsTr("Telegram handle")
return "" return ""
} }

View File

@ -97,6 +97,14 @@ QtObject {
return Style.current.accountColors[colorIndex] return Style.current.accountColors[colorIndex]
} }
function validLink(link) {
if (link.length === 0) {
return false
}
var regex = /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
return regex.test(link)
}
function getLinkStyle(link, hoveredLink, textColor) { function getLinkStyle(link, hoveredLink, textColor) {
return `<style type="text/css">` + return `<style type="text/css">` +
`a {` + `a {` +

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit bf29188b2d5e88dde54549927f41b9c2808bf225 Subproject commit 874a0656db7dfe1a7e0b7e7008c6bef6ac801a40