package gui //broadcastGui.go implements pages for a user to broadcast, disable, and enable their profiles, and to monitor manual broadcasts //TODO: Add review replacement functionality // When broadcasting a review, check if the existing conflicting review exists for the same reviewedhash // If the review is reviewing an identity that the user has already reviewed and broadcast, we should show this in the GUI // The user should confirm to overwrite their previous verdict // Once they agree, the application should delete their old broadcasted review // We should also add the ability to update existing identity reviews with more errant profiles, if more unruleful profiles are discovered import "fyne.io/fyne/v2" import "fyne.io/fyne/v2/canvas" import "fyne.io/fyne/v2/container" import "fyne.io/fyne/v2/data/binding" import "fyne.io/fyne/v2/dialog" import "fyne.io/fyne/v2/layout" import "fyne.io/fyne/v2/theme" import "fyne.io/fyne/v2/widget" import "seekia/resources/currencies" import "seekia/internal/appMemory" import "seekia/internal/encoding" import "seekia/internal/genetics/myPeople" import "seekia/internal/globalSettings" import "seekia/internal/helpers" import "seekia/internal/imagery" import "seekia/internal/mateQuestionnaire" import "seekia/internal/moderation/myIdentityScore" import "seekia/internal/myIdentity" import "seekia/internal/network/appNetworkType/getAppNetworkType" import "seekia/internal/network/manualBroadcasts" import "seekia/internal/network/myBroadcasts" import "seekia/internal/network/myIdentityBalance" import "seekia/internal/parameters/getParameters" import "seekia/internal/profiles/attributeDisplay" import "seekia/internal/profiles/myLocalProfiles" import "seekia/internal/profiles/myProfileExports" import "seekia/internal/profiles/myProfileStatus" import "seekia/internal/profiles/profileFormat" import "seekia/internal/profiles/readProfiles" import "strings" import "image" import "time" import "errors" func setBroadcastPage(window fyne.Window, profileType string, previousPage func()){ setLoadingScreen(window, "Broadcast " + profileType + " Profile", "Loading Broadcast Page...") currentPage := func(){setBroadcastPage(window, profileType, previousPage)} appMemory.SetMemoryEntry("CurrentViewedPage", "Broadcast") if (profileType != "Mate" && profileType != "Moderator"){ setErrorEncounteredPage(window, errors.New("setBroadcastPage called with invalid profile type: " + profileType), previousPage) return } title := getPageTitleCentered("Broadcast " + profileType + " Profile") backButton := getBackButtonCentered(previousPage) myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(profileType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityExists == false){ profileIcon, err := getFyneImageIcon("Profile") if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } iconSize := getCustomFyneSize(10) profileIcon.SetMinSize(iconSize) profileIconCentered := container.NewHBox(layout.NewSpacer(), profileIcon, layout.NewSpacer()) description1 := getBoldLabelCentered("Your " + profileType + " identity does not exist.") description2 := getLabelCentered("You must create your user identity hash.") description3 := getLabelCentered("This hash is how other users will identify you.") createButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){ setChooseNewIdentityHashPage(window, profileType, currentPage, currentPage) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), profileIconCentered, description1, description2, description3, createButton) setPageContent(page, window) return } description := getLabelCentered("Broadcast your " + profileType + " profile to the world.") appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } identityExists, myProfileIsActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (identityExists == false) { setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) return } myProfileStatusLabel := getBoldLabelCentered("My Profile Status:") getProfileStatusSection := func()(*fyne.Container, error){ if (myProfileIsActiveStatus == true){ activeIcon, err := getFyneImageIcon("ToggleOn") if (err != nil) { return nil, err } activeLabel := getBoldLabel("Active") activeLabelWithIcon := container.NewGridWithRows(2, activeIcon, activeLabel) updateProfileButton := widget.NewButtonWithIcon("Update", theme.ViewRefreshIcon(), func(){ setBroadcastMyProfilePage(window, profileType, currentPage, currentPage) }) disableButton := widget.NewButtonWithIcon("Disable", theme.CancelIcon(), func(){ setDisableMyProfilePage(window, profileType, currentPage) }) actionButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, updateProfileButton, disableButton)) profileStatusSection := container.NewVBox(activeLabelWithIcon, widget.NewSeparator(), actionButtonsGrid) return profileStatusSection, nil } // myProfileIsActiveStatus == false // Profile is not active. // Possible reasons: // -Profile was never broadcast // -Identity is not funded // -Profile is disabled // -Profile has expired from network (For Mate profiles only) inactiveIcon, err := getFyneImageIcon("ToggleOff") if (err != nil) { return nil, err } inactiveLabel := getBoldLabel("Inactive") inactiveLabelWithIcon := container.NewGridWithRows(2, inactiveIcon, inactiveLabel) exists, localIsDisabled, err := myLocalProfiles.GetProfileData(profileType, "Disabled") if (err != nil) { return nil, err } if (exists == true && localIsDisabled == "Yes"){ enableProfileButton := getWidgetCentered(widget.NewButton("Enable", func(){ setEnableMyProfilePage(window, profileType, currentPage, currentPage) })) profileStatusSection := container.NewVBox(inactiveLabelWithIcon, widget.NewSeparator(), enableProfileButton) return profileStatusSection, nil } broadcastProfileFunction := func()error{ if (profileType == "Mate"){ myIdentityFound, myIdentityIsActivated, myBalanceIsSufficient, _, _, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType) if (err != nil){ return err } if (myIdentityFound == false){ return errors.New("My identity not found after being found already.") } if (myIdentityIsActivated == false || myBalanceIsSufficient == false) { dialogTitle := translate("Identity Balance Is Insufficient.") dialogMessageA := getLabelCentered(translate("Your identity balance is insufficient.")) dialogMessageB := getLabelCentered(translate("You must spend credit to fund your identity and broadcast a profile.")) dialogMessageC := getLabelCentered(translate("Visit the Manage Identity Balance page to fund your identity.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return nil } } else if (profileType == "Moderator"){ myIdentityFound, _, scoreIsSufficient, _, _, err := myIdentityScore.GetMyIdentityScore() if (err != nil) { return err } if (myIdentityFound == false){ return errors.New("My moderator identity not found after being found already.") } if (scoreIsSufficient == false){ dialogTitle := translate("Identity Score Is Insufficient.") dialogMessageA := getLabelCentered(translate("Your identity score is insufficient.")) dialogMessageB := getLabelCentered(translate("You must send cryptocurrency to fund your identity.")) dialogMessageC := getLabelCentered(translate("Visit the Manage Identity Score page to fund your identity.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return nil } } setBroadcastMyProfilePage(window, profileType, currentPage, currentPage) return nil } broadcastButton := getWidgetCentered(widget.NewButtonWithIcon("Broadcast", theme.RadioButtonCheckedIcon(), func(){ err := broadcastProfileFunction() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } })) profileStatusSection := container.NewVBox(inactiveLabelWithIcon, broadcastButton) return profileStatusSection, nil } profileStatusSection, err := getProfileStatusSection() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), myProfileStatusLabel, widget.NewSeparator(), profileStatusSection, widget.NewSeparator()) if (myProfileIsActiveStatus == true && profileType == "Mate"){ addProfileExpirationTimeSection := func()error{ profileExpirationTimeLabel := getItalicLabelCentered("My Profile Expiration Time:") page.Add(profileExpirationTimeLabel) // All mate profiles will expire after an expiration duration, which is defined in the network parameters // They must be updated, or else they will be dropped by the network // We will determine how long this current profile has until it is expired // The user has to broadcast a profile again to reset this countdown myIdentityExists, myProfileExists, _, myProfileAttributeExists, myProfileCreationTime, err := myBroadcasts.GetAnyAttributeFromMyBroadcastProfile(myIdentityHash, appNetworkType, "CreationTime") if (err != nil) { return err } if (myIdentityExists == false) { return errors.New("My identity not found after being found already.") } if (myProfileExists == false){ return errors.New("My broadcast profile not found after myProfileStatus has already been determined to be active.") } if (myProfileAttributeExists == false){ return errors.New("My Broadcast profile malformed: Missing CreationTime") } myProfileCreationTimeInt64, err := helpers.ConvertCreationTimeStringToInt64(myProfileCreationTime) if (err != nil){ return errors.New("My Broadcast profile malformed: Contains invalid creationTime: " + myProfileCreationTime) } _, mateProfileMaximumExistenceDuration, err := getParameters.GetMateProfileMaximumExistenceDuration(appNetworkType) if (err != nil) { return err } currentTime := time.Now().Unix() profileExistenceTime := currentTime - myProfileCreationTimeInt64 if (profileExistenceTime >= mateProfileMaximumExistenceDuration){ return errors.New("My Broadcast profile is expired, but myProfileStatus says it is active") } timeUntilExpiration := mateProfileMaximumExistenceDuration - profileExistenceTime timeUntilExpirationString, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(timeUntilExpiration, true) if (err != nil) { return err } timeUntilExpirationLabel := getBoldLabel("Expires in " + timeUntilExpirationString) profileExpirationTimeHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setMateProfileExpirationExplainerPage(window, currentPage) }) timeUntilExpirationRow := container.NewHBox(layout.NewSpacer(), timeUntilExpirationLabel, profileExpirationTimeHelpButton, layout.NewSpacer()) page.Add(timeUntilExpirationRow) page.Add(widget.NewSeparator()) return nil } err := addProfileExpirationTimeSection() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } } if (profileType == "Mate"){ getMyIdentityBalanceSection := func()(*fyne.Container, error){ myIdentityBalanceLabel := getItalicLabelCentered("My Identity Balance:") getMyIdentityBalanceStatus := func()(string, error){ myIdentityFound, myIdentityIsActivated, myBalanceIsSufficient, _, myBalanceExpirationTime, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType) if (err != nil){ return "", err } if (myIdentityFound == false){ return "", errors.New("My identity not found after being found already.") } if (myIdentityIsActivated == false || myBalanceIsSufficient == false){ result := translate("Insufficient") return result, nil } currentTime := time.Now().Unix() timeLeft := myBalanceExpirationTime - currentTime timeTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(timeLeft, true) if (err != nil) { return "", err } identityBalanceExpirationTime := "Expires in " + timeTranslated return identityBalanceExpirationTime, nil } myIdentityBalanceStatus, err := getMyIdentityBalanceStatus() if (err != nil){ return nil, err } myIdentityBalanceStatusLabel := getBoldLabelCentered(myIdentityBalanceStatus) manageBalanceButton := getWidgetCentered(widget.NewButtonWithIcon("Manage", theme.VisibilityIcon(), func(){ setViewMyIdentityBalancePage(window, profileType, currentPage) })) identityBalanceSection := container.NewVBox(myIdentityBalanceLabel, myIdentityBalanceStatusLabel, manageBalanceButton) return identityBalanceSection, nil } myIdentityBalanceSection, err := getMyIdentityBalanceSection() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } page.Add(myIdentityBalanceSection) } else if (profileType == "Moderator"){ getIdentityScoreSection := func()(*fyne.Container, error){ myIdentityScoreLabel := getItalicLabelCentered("My Identity Score:") myIdentityFound, _, scoreIsSufficient, _, _, err := myIdentityScore.GetMyIdentityScore() if (err != nil) { return nil, err } if (myIdentityFound == false){ return nil, errors.New("My identity not found after being found already.") } getIdentityScoreStatus := func()string{ if (scoreIsSufficient == false){ result := translate("Insufficient") return result } result := translate("Sufficient") return result } identityScoreStatus := getIdentityScoreStatus() identityScoreStatusLabel := getBoldLabelCentered(identityScoreStatus) manageIdentityScoreButton := getWidgetCentered(widget.NewButtonWithIcon("View Identity Score", theme.VisibilityIcon(), func(){ setViewMyModeratorScorePage(window, currentPage) })) identityScoreSection := container.NewVBox(myIdentityScoreLabel, identityScoreStatusLabel, manageIdentityScoreButton) return identityScoreSection, nil } identityScoreSection, err := getIdentityScoreSection() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } page.Add(identityScoreSection) } setPageContent(page, window) } func setBroadcastMyProfilePage(window fyne.Window, profileType string, previousPage func(), pageToVisitAfter func()) { currentPage := func(){setBroadcastMyProfilePage(window, profileType, previousPage, pageToVisitAfter)} title := getPageTitleCentered(translate("Broadcast " + profileType + " Profile")) backButton := getBackButtonCentered(previousPage) if (profileType == "Mate"){ myGenomePersonIdentifierExists, myGenomePersonIdentifier, err := myLocalProfiles.GetProfileData("Mate", "GenomePersonIdentifier") if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (myGenomePersonIdentifierExists == true){ anyGenomesExist, personAnalysisIsReady, _, err := myPeople.CheckIfPersonAnalysisIsReady(myGenomePersonIdentifier) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (anyGenomesExist == true && personAnalysisIsReady == false){ description1 := getBoldLabelCentered(translate("Your profile contains a linked genome person.")) description2 := getLabelCentered(translate("You need to perform your genetic analysis.")) description3 := getLabelCentered(translate("Only the information you choose will be shared in your profile.")) performAnalysisButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Perform Analysis"), theme.NavigateNextIcon(), func(){ setConfirmPerformPersonAnalysisPage(window, myGenomePersonIdentifier, currentPage, currentPage) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, performAnalysisButton) setPageContent(page, window) return } } } appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } err = myProfileExports.UpdateMyExportedProfile(profileType, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(profileType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityExists == false){ setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) return } existingBroadcastProfileExists, _, _, _, existingBroadcastProfileRawMap, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } page := container.NewVBox(title, backButton, widget.NewSeparator()) if (existingBroadcastProfileExists == true){ description1 := getBoldLabelCentered("Are you sure you want to update your " + profileType + " profile?") page.Add(description1) } else { description1 := getBoldLabelCentered("Are you sure you want to broadcast your " + profileType + " profile?") page.Add(description1) } viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ setViewMyProfilePage(window, profileType, "Local", currentPage) })) page.Add(viewProfileButton) page.Add(widget.NewSeparator()) if (existingBroadcastProfileExists == true){ getLastUpdatedTimeAgo := func()(string, error){ exists, lastCreationTimeString, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(existingBroadcastProfileRawMap, "CreationTime") if (err != nil){ return "", err } if (exists == false){ return "", errors.New("My broadcast profile missing CreationTime") } lastCreationTimeInt64, err := helpers.ConvertStringToInt64(lastCreationTimeString) if (err != nil){ return "", errors.New("My broadcast profile contains invalid CreationTime: " + lastCreationTimeString) } lastUpdatedTimeAgo, err := helpers.ConvertUnixTimeToTimeAgoTranslated(lastCreationTimeInt64, true) if (err != nil){ return "", err } return lastUpdatedTimeAgo, nil } lastUpdatedTimeAgo, err := getLastUpdatedTimeAgo() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } lastUpdatedTitle := widget.NewLabel("Last Updated:") lastUpdatedLabel := getBoldLabel(lastUpdatedTimeAgo) lastUpdatedRow := container.NewHBox(layout.NewSpacer(), lastUpdatedTitle, lastUpdatedLabel, layout.NewSpacer()) page.Add(lastUpdatedRow) getNumberOfChangesMade := func()(int, error){ profileFound, _, _, exportProfileRawMap, err := myProfileExports.GetMyExportedProfile(profileType, appNetworkType) if (err != nil) { return 0, err } if (profileFound == false){ return 0, errors.New("My exported profile not found after profile was exported.") } allAttributesMap := make(map[int]struct{}) for attributeIdentifier, _ := range existingBroadcastProfileRawMap{ allAttributesMap[attributeIdentifier] = struct{}{} } for attributeIdentifier, _ := range exportProfileRawMap{ allAttributesMap[attributeIdentifier] = struct{}{} } changesMade := 0 // This will add the number of changed fields for attributeIdentifier, _ := range allAttributesMap{ attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) if (err != nil) { return 0, err } if (attributeName == "CreationTime" || attributeName == "ChatKeysLatestUpdateTime" || attributeName == "NaclKey" || attributeName == "KyberKey"){ continue } existingValueExists, existingValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(existingBroadcastProfileRawMap, attributeName) if (err != nil) { return 0, err } exportValueExists, exportValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(exportProfileRawMap, attributeName) if (err != nil) { return 0, err } if (existingValueExists != exportValueExists || existingValue != exportValue){ // The value has changed between the old and new profile changesMade += 1 } } return changesMade, nil } numberOfChangesMade, err := getNumberOfChangesMade() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } numberOfChangesMadeString := helpers.ConvertIntToString(numberOfChangesMade) changesMadeTitle := getItalicLabel("Changes Made:") numberOfChangesMadeLabel := getBoldLabel(numberOfChangesMadeString) numberOfChangesMadeRow := container.NewHBox(layout.NewSpacer(), changesMadeTitle, numberOfChangesMadeLabel, layout.NewSpacer()) page.Add(numberOfChangesMadeRow) if (numberOfChangesMade != 0){ viewChangesButton := getWidgetCentered(widget.NewButtonWithIcon("View Changes", theme.VisibilityIcon(), func(){ setViewNewProfileChangesPage(window, profileType, currentPage) })) page.Add(viewChangesButton) } page.Add(widget.NewSeparator()) } if (profileType == "Mate"){ // Mate profiles must be funded with each broadcast getCurrentAppCurrency := func()(string, error){ exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") if (err != nil) { return "", err } if (exists == false){ return "USD", nil } return currentAppCurrency, nil } currentAppCurrencyCode, err := getCurrentAppCurrency() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } costTitle := widget.NewLabel("Cost:") appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ setChangeAppCurrencyPage(window, currentPage) }) //TODO: Fix this to actually calculate cost (get cost from parameters) costLabel := getBoldLabel("0.1 " + currentAppCurrencyCode) costRow := container.NewHBox(layout.NewSpacer(), costTitle, appCurrencySymbolButton, costLabel, layout.NewSpacer()) page.Add(costRow) page.Add(widget.NewSeparator()) } broadcastIcon, err := getFyneImageIcon("Broadcast") if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } confirmButton := widget.NewButtonWithIcon("Broadcast", theme.ConfirmIcon(), func(){ //TODO: Make sure credit is available setConfirmBroadcastMyProfilePage(window, profileType, currentPage) }) confirmButtonWithIcon := getContainerCentered(container.NewGridWithRows(2, broadcastIcon, confirmButton)) page.Add(confirmButtonWithIcon) page.Add(widget.NewSeparator()) setPageContent(page, window) } func setConfirmBroadcastMyProfilePage(window fyne.Window, profileType string, previousPage func()){ if (profileType != "Mate" && profileType != "Moderator"){ setErrorEncounteredPage(window, errors.New("setConfirmBroadcastMyProfilePage called with invalid profileType: " + profileType), previousPage) return } title := getPageTitleCentered("Confirm Broadcast Profile") backButton := getBackButtonCentered(previousPage) //TODO: Improve wording of below: description1 := getBoldLabelCentered("Confirm broadcast your " + profileType + " profile?") description2 := getLabelCentered("This will upload your profile to the Seekia network.") description3 := getLabelCentered("The whole world will be able to see it.") description4 := getLabelCentered("Make sure you are comfortable sharing your profile with the world.") confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ profilePage := func(){setProfilePage(window, true, profileType, false, nil)} afterCompletionPage := func(){setBroadcastPage(window, profileType, profilePage)} if (profileType == "Mate"){ setStartAndMonitorMateProfileFundingAndBroadcastPage(window, afterCompletionPage) } else if (profileType == "Moderator"){ setStartAndMonitorMyModeratorProfileBroadcastPage(window, afterCompletionPage) } })) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, confirmButton) setPageContent(page, window) } // This function will show the changes between the existing broadcast profile and the new exported profile func setViewNewProfileChangesPage(window fyne.Window, myProfileType string, previousPage func()){ setLoadingScreen(window, "View Profile Changes", "Loading profile changes...") currentPage := func(){setViewNewProfileChangesPage(window, myProfileType, previousPage)} title := getPageTitleCentered("View Profile Changes") backButton := getBackButtonCentered(previousPage) description := getLabelCentered("Below are the changes between your old and new profile.") getChangesGrid := func()(*fyne.Container, error){ myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) if (err != nil){ return nil, err } if (myIdentityExists == false){ return nil, errors.New("setViewNewProfileChangesPage called with missing identity.") } appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { return nil, err } oldProfileExists, _, _, _, oldProfileRawMap, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) if (err != nil) { return nil, err } if (oldProfileExists == false){ return nil, errors.New("setViewNewProfileChangesPage called when no broadcast profile exists.") } profileFound, _, _, newProfileRawMap, err := myProfileExports.GetMyExportedProfile(myProfileType, appNetworkType) if (err != nil) { return nil, err } if (profileFound == false){ return nil, errors.New("setViewNewProfileChangesPage called when exportedProfile is missing.") } // We use this map to avoid duplicates allAttributesIdentifiersMap := make(map[int]struct{}) for attributeIdentifier, _ := range oldProfileRawMap{ allAttributesIdentifiersMap[attributeIdentifier] = struct{}{} } for attributeIdentifier, _ := range newProfileRawMap{ allAttributesIdentifiersMap[attributeIdentifier] = struct{}{} } allAttributeNamesList := make([]string, 0, len(allAttributesIdentifiersMap)) for attributeIdentifier, _ := range allAttributesIdentifiersMap{ attributeName, err := profileFormat.GetAttributeNameFromAttributeIdentifier(attributeIdentifier) if (err != nil) { return nil, err } allAttributeNamesList = append(allAttributeNamesList, attributeName) } // We sort attributes so they show up in the same order each time helpers.SortStringListToUnicodeOrder(allAttributeNamesList) attributeLabel := getItalicLabelCentered("Attribute") oldProfileTitle := getItalicLabelCentered("Old Profile") newProfileTitle := getItalicLabelCentered("New Profile") attributeTitleColumn := container.NewVBox(attributeLabel, widget.NewSeparator()) oldProfileColumn := container.NewVBox(oldProfileTitle, widget.NewSeparator()) newProfileColumn := container.NewVBox(newProfileTitle, widget.NewSeparator()) for _, attributeName := range allAttributeNamesList{ if (attributeName == "CreationTime" || attributeName == "Disabled" || attributeName == "ChatKeysLatestUpdateTime" || attributeName == "NaclKey" || attributeName == "KyberKey"){ continue } oldValueExists, oldValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(oldProfileRawMap, attributeName) if (err != nil) { return nil, err } newValueExists, newValue, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(newProfileRawMap, attributeName) if (err != nil) { return nil, err } if (oldValueExists == false && newValueExists == false){ // This should never happen. Probably cosmic ray bit flip or faulty hardware. return nil, errors.New("oldProfileRawMap and newProfileRawMap are missing the attribute.") } if (oldValueExists == true && newValueExists == true && oldValue == newValue){ // The value is the same between both profiles continue } attributeTitle, _, formatValueFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) if (err != nil){ return nil, err } attributeTitleLabel := getBoldLabelCentered(attributeTitle) getProfileValueCell := func(valueExists bool, profileValue string)(*fyne.Container, error){ if (valueExists == false){ noneLabel := getItalicLabelCentered(translate("None")) return noneLabel, nil } switch attributeName{ case "Photos":{ photosBase64List := strings.Split(profileValue, "+") photosList := make([]image.Image, 0, len(photosBase64List)) for _, photoBase64 := range photosBase64List{ goImage, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(photoBase64) if (err != nil){ return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Photos attribute: " + profileValue) } photosList = append(photosList, goImage) } if (len(photosList) > 5){ return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Photos attribute: Too many photos: " + profileValue) } viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewFullpageImagesWithNavigationPage(window, photosList, 0, currentPage) })) return viewAttributeButton, nil } case "Avatar":{ avatarIdentifier, err := helpers.ConvertStringToInt(profileValue) if (err != nil) { return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Avatar attribute: " + profileValue) } emojiImage, err := getEmojiImageObject(avatarIdentifier) if (err != nil){ return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Avatar attribute: " + profileValue) } viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewFullpageImagePage(window, emojiImage, currentPage) })) return viewAttributeButton, nil } case "23andMe_AncestryComposition":{ viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewUser23andMeAncestryCompositionPage(window, profileValue, currentPage) })) return viewAttributeButton, nil } case "Questionnaire":{ questionnaireObject, err := mateQuestionnaire.ReadQuestionnaireString(profileValue) if (err != nil) { return nil, errors.New("setViewNewProfileChangesPage called with profile containing invalid Questionnaire attribute: " + profileValue + ". Reason: " + err.Error()) } myResponsesMap := make(map[string]string) submitQuestionnairePage := func(_ string, _ func()){ currentPage() } viewAttributeButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setTakeQuestionnairePage(window, questionnaireObject, 0, myResponsesMap, currentPage, submitQuestionnairePage) })) return viewAttributeButton, nil } //TODO: Format more values that cannot be displayed as a string in their raw form (Tags, Location, Language, etc...) } valueFormatted, err := formatValueFunction(profileValue) if (err != nil) { return nil, err } valueTrimmed, anyChangesOccurred, err := helpers.TrimAndFlattenString(valueFormatted, 20) if (err != nil) { return nil, err } if (anyChangesOccurred == false){ valueLabel := getBoldLabelCentered(valueFormatted) return valueLabel, nil } trimmedValueLabel := getBoldLabel(valueTrimmed) viewFullValueButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewTextPage(window, "Viewing Attribute", valueFormatted, false, currentPage) }) profileValueCell := container.NewHBox(layout.NewSpacer(), trimmedValueLabel, viewFullValueButton, layout.NewSpacer()) return profileValueCell, nil } oldValueCell, err := getProfileValueCell(oldValueExists, oldValue) if (err != nil) { return nil, err } newValueCell, err := getProfileValueCell(newValueExists, newValue) if (err != nil) { return nil, err } attributeTitleColumn.Add(attributeTitleLabel) oldProfileColumn.Add(oldValueCell) newProfileColumn.Add(newValueCell) attributeTitleColumn.Add(widget.NewSeparator()) oldProfileColumn.Add(widget.NewSeparator()) newProfileColumn.Add(widget.NewSeparator()) } displayGrid := container.NewHBox(layout.NewSpacer(), attributeTitleColumn, oldProfileColumn, newProfileColumn, layout.NewSpacer()) return displayGrid, nil } displayGrid, err := getChangesGrid() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), displayGrid) setPageContent(page, window) } func setEnableMyProfilePage(window fyne.Window, myProfileType string, previousPage func(), pageToVisitAfter func()){ if (myProfileType != "Mate" && myProfileType != "Moderator"){ setErrorEncounteredPage(window, errors.New("setEnableMyProfilePage called with invalid profileType: " + myProfileType), previousPage) return } myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityExists == false){ // This should not happen, this page should only be reached if identity exists setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) return } attributeExists, localProfileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled") if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (attributeExists == false || localProfileIsDisabled != "Yes"){ // This should not happen, as this page should only be viewed if profile is disabled setErrorEncounteredPage(window, errors.New("setEnableMyProfilePage called when your profile is not disabled."), previousPage) return } title := getPageTitleCentered("Enable " + myProfileType + " Profile") backButton := getBackButtonCentered(previousPage) description1 := getBoldLabelCentered("Are you sure you want to re-enable your profile?") description2 := getLabelCentered("You must broadcast your profile after this step.") confirmFunction := func(){ err = myBroadcasts.DeleteMyBroadcastProfiles(myIdentityHash) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } err := myLocalProfiles.SetProfileData(myProfileType, "Disabled", "No") if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } pageToVisitAfter() } confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), confirmFunction)) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton) setPageContent(page, window) } func setDisableMyProfilePage(window fyne.Window, myProfileType string, previousPage func()){ if (myProfileType != "Mate" && myProfileType != "Moderator"){ setErrorEncounteredPage(window, errors.New("setDisableMyProfilePage called with invalid profile type: " + myProfileType), previousPage) return } myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myProfileType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityExists == false){ // This should not happen, this page should only be reached if identity exists setErrorEncounteredPage(window, errors.New("My identity not found on setDisableMyProfilePage."), previousPage) return } currentPage := func(){setDisableMyProfilePage(window, myProfileType, previousPage)} title := getPageTitleCentered("Disable My " + myProfileType + " Profile") backButton := getBackButtonCentered(previousPage) getLocalProfileIsDisabledStatus := func()(bool, error){ attributeExists, localProfileIsDisabled, err := myLocalProfiles.GetProfileData(myProfileType, "Disabled") if (err != nil){ return false, err } if (attributeExists == true && localProfileIsDisabled == "Yes"){ return true, nil } return false, nil } appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } getBroadcastProfileIsDisabledStatus := func()(bool, error){ identityExists, profileExists, _, attributeExists, broadcastProfileIsDisabled, err := myBroadcasts.GetAnyAttributeFromMyBroadcastProfile(myIdentityHash, appNetworkType, "Disabled") if (err != nil) { return false, err } if (identityExists == false) { return false, errors.New("My identity not found after being found already.") } if (profileExists == false){ return false, nil } if (attributeExists == true && broadcastProfileIsDisabled == "Yes"){ return true, nil } return false, nil } localProfileIsDisabled, err := getLocalProfileIsDisabledStatus() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } broadcastProfileIsDisabled, err := getBroadcastProfileIsDisabledStatus() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (localProfileIsDisabled == true && broadcastProfileIsDisabled == true){ // This should not happen, as this page should only be viewed if profile is enabled setErrorEncounteredPage(window, errors.New("setDisableMyProfilePage accessed with enabled/missing profile."), previousPage) return } description1 := getBoldLabelCentered("Confirm to disable your " + myProfileType + " Profile?") description2 := getLabelCentered("Other users will see your profile as disabled.") description3 := getLabelCentered("You will stop receiving " + myProfileType + " messages.") description4 := getLabelCentered("You can always enable your profile later.") page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4) if (myProfileType == "Mate"){ description5 := getLabelCentered("Your identity balance will be retained, but will continue to expire.") page.Add(description5) } else if (myProfileType == "Moderator"){ description5 := getLabelCentered("Your identity score will be retained.") page.Add(description5) } if (myProfileType == "Mate"){ // Mate profiles must be funded with each broadcast page.Add(widget.NewSeparator()) getCurrentAppCurrency := func()(string, error){ exists, currentAppCurrency, err := globalSettings.GetSetting("Currency") if (err != nil) { return "", err } if (exists == false){ return "USD", nil } return currentAppCurrency, nil } currentAppCurrencyCode, err := getCurrentAppCurrency() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } costTitle := widget.NewLabel("Cost:") appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ setChangeAppCurrencyPage(window, currentPage) }) //TODO: Fix this to actually calculate cost (get cost from parameters) costLabel := getBoldLabel("0.10 " + currentAppCurrencyCode) costRow := container.NewHBox(layout.NewSpacer(), costTitle, appCurrencySymbolButton, costLabel, layout.NewSpacer()) page.Add(costRow) page.Add(widget.NewSeparator()) } confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ //TODO: Make sure credit is sufficient (if profileType == "Mate") err := myLocalProfiles.SetProfileData(myProfileType, "Disabled", "Yes") if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } err = myProfileExports.UpdateMyExportedProfile(myProfileType, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (myProfileType == "Mate"){ setStartAndMonitorMateProfileFundingAndBroadcastPage(window, previousPage) } else if (myProfileType == "Moderator"){ setStartAndMonitorMyModeratorProfileBroadcastPage(window, previousPage) } })) // TODO: Add a warning that you should not disable multiple identity's profiles at the same time // Correlation between the identities is possible page.Add(confirmButton) setPageContent(page, window) } // This page will first attempt to fund a mate profile, and if it succeeds, it will initiate a manual profile broadcast func setStartAndMonitorMateProfileFundingAndBroadcastPage(window fyne.Window, afterCompletionPage func()){ pageIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { setErrorEncounteredPage(window, err, afterCompletionPage) return } appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) checkIfPageHasChangedFunction := func()bool{ exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") if (exists == true && currentViewedPage == pageIdentifier){ return false } return true } title := getPageTitleCentered("Funding Mate Profile") progressBinding := binding.NewString() updateProgressBindings := func(){ startTime := time.Now().Unix() for { //TODO: Add details that describe the steps (Example: contacting server, making transaction) currentTime := time.Now().Unix() secondsElapsed := currentTime - startTime if (secondsElapsed % 3 == 0){ progressBinding.Set("Funding profile.") } else if (secondsElapsed % 3 == 1){ progressBinding.Set("Funding profile..") } else { progressBinding.Set("Funding profile...") } pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } time.Sleep(time.Second/2) } } appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, afterCompletionPage) return } fundProfileFunction := func(){ // We fund the export profile before we broadcast it newProfileFound, newProfileHash, _, _, err := myProfileExports.GetMyExportedProfile("Mate", appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, afterCompletionPage) return } if (newProfileFound == false){ setErrorEncounteredPage(window, errors.New("setStartAndMonitorMateProfileFundingAndBroadcastPage called when export profile is missing."), afterCompletionPage) return } //Outputs: // -bool: Fund successful // -error fundProfile := func(profileHashToFund [28]byte)(bool, error){ //TODO: Add function to fund profile time.Sleep(time.Second * 3) return true, nil } fundSuccessful, err := fundProfile(newProfileHash) if (err != nil){ pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } setErrorEncounteredPage(window, errors.New("Profile fund encountered error: " + err.Error()), afterCompletionPage) return } if (fundSuccessful == false){ pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } description1 := getBoldLabelCentered("Seekia failed to fund the profile.") description2 := getLabelCentered("The account credit server we contacted may be down.") description3 := getLabelCentered("Your internet connection may also be broken.") retryFunction := func(){setStartAndMonitorMateProfileFundingAndBroadcastPage(window, afterCompletionPage)} retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction)) exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) manageConnectionDescription := getLabelCentered("Check if your internet connection is working below.") manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){ setManageNetworkConnectionPage(window, retryFunction) })) page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), retryButton, exitButton, widget.NewSeparator(), manageConnectionDescription, manageConnectionButton) setPageContent(page, window) return } // Fund is complete. We update the broadcast profile myIdentityExists, newBroadcastProfileHash, err := myBroadcasts.UpdateMyBroadcastProfile("Mate", appNetworkType) if (err != nil) { pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } setErrorEncounteredPage(window, err, afterCompletionPage) return } if (myIdentityExists == false){ pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), afterCompletionPage) return } if (newBroadcastProfileHash != newProfileHash){ pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } // This should not happen. This means we funded the wrong profile. // MyBroadcasts broadcasts the newest exported profile setErrorEncounteredPage(window, errors.New("Exported profile is not the same as broadcasted profile"), afterCompletionPage) return } // Now we start a new broadcast //Outputs: // -bool: Any hosts found // -[22]byte: New process identifier // -error startNewBroadcastFunction := func()(bool, [22]byte, error){ myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate") if (err != nil){ return false, [22]byte{}, err } if (myIdentityExists == false){ return false, [22]byte{}, errors.New("My identity not found after being found already.") } // We get the user's newest profile each time // It is unlikely, but the user could update their broadcast profile before this manual broadcast process completes // In this case, we will just be broadcasting the user's newest profile in multiple manual processes profileExists, _, profileHash, profileBytes, _, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) if (err != nil) { return false, [22]byte{}, err } if (profileExists == false){ return false, [22]byte{}, errors.New("Cannot get profile to broadcast on setStartAndMonitorMateProfileFundingAndBroadcastPage") } if (profileHash != newProfileHash){ return false, [22]byte{}, errors.New("GetMyNewestBroadcastProfile returning different profile during Mate profile broadcast") } profileToBroadcastList := [][]byte{profileBytes} anyHostsFound, processIdentifier, err := manualBroadcasts.StartContentBroadcast("Profile", appNetworkType, profileToBroadcastList, 3) if (err != nil) { return false, [22]byte{}, err } if (anyHostsFound == false){ return false, [22]byte{}, nil } return true, processIdentifier, nil } anyHostsFound, newProcessIdentifier, err := startNewBroadcastFunction() if (err != nil){ pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } setErrorEncounteredPage(window, err, afterCompletionPage) return } pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ // Whether the process started or not, we will not show the user in the GUI // If no hosts were found, their profile should still be broadcast in the background // Seekia will automatically download enough hosts to contact return } noHostsFound := !anyHostsFound nextPageTitle := "Broadcasting Mate Profile" nextPageDescription := "Seekia is broadcasting your Mate profile." setMonitorManualBroadcastPage(window, nextPageTitle, "Profile", nextPageDescription, noHostsFound, newProcessIdentifier, startNewBroadcastFunction, afterCompletionPage) } description1 := getBoldLabelCentered("Seekia is funding your new Mate profile.") description2 := getLabelCentered("You can leave this page.") progressLabel := widget.NewLabelWithData(progressBinding) progressLabel.TextStyle = getFyneTextStyle_Bold() progressLabelCentered := getWidgetCentered(progressLabel) exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) page := container.NewVBox(title, widget.NewSeparator(), description1, description2, widget.NewSeparator(), progressLabelCentered, widget.NewSeparator(), exitButton) setPageContent(page, window) go updateProgressBindings() go fundProfileFunction() } // Starts broadcast and allows user to monitor progress func setStartAndMonitorMyModeratorProfileBroadcastPage(window fyne.Window, afterCompletionPage func()){ myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Moderator") if (err != nil){ setErrorEncounteredPage(window, err, afterCompletionPage) return } if (myIdentityExists == false){ setErrorEncounteredPage(window, errors.New("Identity does not exist on setStartAndMonitorMyModeratorProfileBroadcastPage"), afterCompletionPage) return } // We have to update the broadcast profile appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, afterCompletionPage) return } identityExists, _, err := myBroadcasts.UpdateMyBroadcastProfile("Moderator", appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, afterCompletionPage) return } if (identityExists == false){ setErrorEncounteredPage(window, errors.New("Identity not found after being found already."), afterCompletionPage) return } //Outputs: // -bool: Any hosts found // -[22]byte: New process identifier // -error startNewBroadcastFunction := func()(bool, [22]byte, error){ // We get the user's newest profile each time // It is unlikely, but the user could update their broadcast profile before this manual broadcast process completes // In this case, we will just be broadcasting the user's newest profile in multiple manual processes profileExists, _, _, profileBytes, _, err := myBroadcasts.GetMyNewestBroadcastProfile(myIdentityHash, appNetworkType) if (err != nil) { return false, [22]byte{}, err } if (profileExists == false){ return false, [22]byte{}, errors.New("Cannot get profile to broadcast on setStartAndMonitorMyModeratorProfileBroadcastPage") } profileToBroadcastList := [][]byte{profileBytes} anyHostsFound, processIdentifier, err := manualBroadcasts.StartContentBroadcast("Profile", appNetworkType, profileToBroadcastList, 3) if (err != nil) { return false, [22]byte{}, err } if (anyHostsFound == false){ return false, [22]byte{}, nil } return true, processIdentifier, nil } anyHostsFound, newProcessIdentifier, err := startNewBroadcastFunction() if (err != nil){ setErrorEncounteredPage(window, err, afterCompletionPage) return } noHostsFound := !anyHostsFound nextPageTitle := "Broadcasting Moderator Profile" nextPageDescription := "Seekia is broadcasting your Moderator profile." setMonitorManualBroadcastPage(window, nextPageTitle, "Profile", nextPageDescription, noHostsFound, newProcessIdentifier, startNewBroadcastFunction, afterCompletionPage) } func setMonitorManualBroadcastPage(window fyne.Window, pageTitleText string, broadcastType string, description1Text string, noHostsFound bool, processIdentifier [22]byte, startNewBroadcastFunction func()(bool, [22]byte, error), afterCompletionPage func()){ currentPage := func(){setMonitorManualBroadcastPage(window, pageTitleText, broadcastType, description1Text, noHostsFound, processIdentifier, startNewBroadcastFunction, afterCompletionPage)} pageIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { setErrorEncounteredPage(window, err, afterCompletionPage) return } appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) checkIfPageHasChangedFunction := func()bool{ exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") if (exists == true && currentViewedPage == pageIdentifier){ return false } return true } title := getPageTitleCentered(pageTitleText) retryFunction := func(){ newBroadcastAnyHostsFound, newProcessIdentifier, err := startNewBroadcastFunction() if (err != nil){ setErrorEncounteredPage(window, err, afterCompletionPage) return } newBroadcastNoHostsFound := !newBroadcastAnyHostsFound setMonitorManualBroadcastPage(window, pageTitleText, broadcastType, description1Text, newBroadcastNoHostsFound, newProcessIdentifier, startNewBroadcastFunction, afterCompletionPage) } if (noHostsFound == true){ description1 := getLabelCentered("No available hosts were found.") description2 := getLabelCentered("Please wait for Seekia to find more hosts.") description3 := getLabelCentered("This should take less than 1 minute.") description4 := getLabelCentered("You can leave this page and the broadcast will still happen automatically.") retryingInSecondsBinding := binding.NewString() startRetryCountdownFunction := func(){ secondsRemaining := 30 for { secondsRemainingString := helpers.ConvertIntToString(secondsRemaining) if (secondsRemaining != 1){ retryingInSecondsBinding.Set("Retrying in " + secondsRemainingString + " seconds...") } else { retryingInSecondsBinding.Set("Retrying in " + secondsRemainingString + " second...") } time.Sleep(time.Second) secondsRemaining -= 1 if (secondsRemaining <= 0){ pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } retryFunction() return } } } retryingInLabel := widget.NewLabelWithData(retryingInSecondsBinding) retryingInLabel.TextStyle = getFyneTextStyle_Bold() retryingInLabelCentered := getWidgetCentered(retryingInLabel) retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction)) exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) descriptionD := getLabelCentered("Check if your internet connection is working below.") manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){ setManageNetworkConnectionPage(window, currentPage) })) page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), retryingInLabelCentered, widget.NewSeparator(), retryButton, exitButton, widget.NewSeparator(), descriptionD, manageConnectionButton) setPageContent(page, window) go startRetryCountdownFunction() return } broadcastProgressStatusBinding := binding.NewString() broadcastProgressDetailsBinding := binding.NewString() updateBindingsFunction := func(){ startTime := time.Now().Unix() setBroadcastProgressStatus := func(processComplete bool, newStatus string){ getProgressEllipsis := func()string{ if (processComplete == true){ return "" } currentTime := time.Now().Unix() secondsElapsed := currentTime - startTime if (secondsElapsed % 3 == 0){ return "." } if (secondsElapsed % 3 == 1){ return ".." } return "..." } progressEllipsis := getProgressEllipsis() broadcastProgressStatusBinding.Set(newStatus + progressEllipsis) } for { processFound, processIsCompleteBool, processEncounteredError, processError, numberOfCompletedBroadcasts, processProgressDetails := manualBroadcasts.GetProcessInfo(processIdentifier) if (processFound == false){ // This should not happen processIdentifierHex := encoding.EncodeBytesToHexString(processIdentifier[:]) setBroadcastProgressStatus(true, "ERROR: manualBroadcasts process not found: " + processIdentifierHex) broadcastProgressDetailsBinding.Set("Report this error to the Seekia developers.") return } numberOfCompletedBroadcastsString := helpers.ConvertIntToString(numberOfCompletedBroadcasts) if (processIsCompleteBool == true){ if (processEncounteredError == true){ setBroadcastProgressStatus(true, "ERROR:" + processError.Error()) broadcastProgressDetailsBinding.Set("Report this error to the Seekia developers.") return } pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } if (numberOfCompletedBroadcasts == 3){ // We broadcasted to all 3 hosts. Broadcast is complete. afterCompletionPage() return } // Broadcast did not complete all required hosts. // We will show user option to retry. retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction)) exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), afterCompletionPage)) if (numberOfCompletedBroadcasts != 0){ // Process is complete, and at least 1 host was broadcasted to, but not all hosts. // This means that we ran out of hosts. // Seekia will keep broadcasting the content(s) in the background, so nothing needs to be done by the user. description1 := getBoldLabelCentered("The broadcast was successful to " + numberOfCompletedBroadcastsString + "/3 hosts.") description2 := getLabelCentered("We ran out of hosts to contact.") description3 := getLabelCentered("You can exit or wait for more hosts to be found and retry.") description4 := getLabelCentered("Seekia will broadcast the " + broadcastType + " in the background either way.") page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, retryButton, exitButton) setPageContent(page, window) return } // Broadcast completed, but 0 hosts were successfully contacted // Now we will show them a "Broadcast Failed" page and an option to retry. description1 := getBoldLabelCentered("The broadcast was unsuccessful.") description2 := getLabelCentered("Retry the broadcast?") descriptionC := getLabelCentered("Check if your internet connection is working below.") manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){ setManageNetworkConnectionPage(window, currentPage) })) page := container.NewVBox(title, widget.NewSeparator(), description1, description2, retryButton, exitButton, widget.NewSeparator(), descriptionC, manageConnectionButton) setPageContent(page, window) return } // Broadcast is not complete progressProgressStatusString := "Broadcasted to " + numberOfCompletedBroadcastsString + "/3 hosts." setBroadcastProgressStatus(processIsCompleteBool, progressProgressStatusString) broadcastProgressDetailsBinding.Set(processProgressDetails) time.Sleep(100 * time.Millisecond) } } description1 := getBoldLabelCentered(description1Text) description2 := getLabelCentered("This process will run in the background.") description3 := getLabelCentered("You can leave this page.") broadcastProgressStatusLabel := widget.NewLabelWithData(broadcastProgressStatusBinding) broadcastProgressStatusLabel.TextStyle = getFyneTextStyle_Bold() broadcastProgressStatusLabelCentered := getWidgetCentered(broadcastProgressStatusLabel) broadcastProgressDetailsLabel := getWidgetCentered(widget.NewLabelWithData(broadcastProgressDetailsBinding)) exitPageButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.MediaSkipNextIcon(), func(){ appMemory.DeleteMemoryEntry("CurrentViewedPage") afterCompletionPage() })) page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), broadcastProgressStatusLabelCentered, broadcastProgressDetailsLabel, widget.NewSeparator(), exitPageButton) setPageContent(page, window) go updateBindingsFunction() } func setViewMyIdentityBalancePage(window fyne.Window, myIdentityType string, previousPage func()){ if (myIdentityType != "Mate" && myIdentityType != "Host"){ setErrorEncounteredPage(window, errors.New("setViewMyIdentityBalancePage called with invalid identity type: " + myIdentityType), previousPage) return } currentPage := func(){setViewMyIdentityBalancePage(window, myIdentityType, previousPage)} title := getPageTitleCentered("My " + myIdentityType + " Identity Balance") backButton := getBackButtonCentered(previousPage) myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(myIdentityType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityExists == false){ // This should not occur, this page should only be reached if identity exists. setErrorEncounteredPage(window, errors.New("Identity not found."), previousPage) return } getDescriptionSection := func()*fyne.Container{ if (myIdentityType == "Mate"){ description1 := getLabelCentered("Below is your Mate identity balance.") description2 := getLabelCentered("It must be sufficient for your profile to be broadcast.") description3 := getLabelCentered("Spend credit to increase your identity balance.") description4 := getLabelCentered("You can be gifted credit or buy some with cryptocurrency.") descriptionSection := container.NewVBox(description1, description2, description3, description4) return descriptionSection } // myIdentityType == "Host" description1 := getLabelCentered("Below is your Host identity balance.") description2 := getLabelCentered("It must be sufficient for you to be a Seekia host.") description3 := getLabelCentered("Spend credit to increase your identity balance.") description4 := getLabelCentered("You can be gifted credit or buy some with cryptocurrency.") descriptionSection := container.NewVBox(description1, description2, description3, description4) return descriptionSection } descriptionSection := getDescriptionSection() getMyIdentityExpirationTimeDisplaySection := func()(*fyne.Container, error){ appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { return nil, err } identityFound, identityIsActivated, balanceIsSufficient, _, balanceExpirationTime, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType) if (err != nil) { return nil, err } if (identityFound == false) { return nil, errors.New("Identity found not found after being found already.") } currentBalanceStatusText := getBoldLabelCentered("My Balance Status:") getBalanceStatusIcon := func()(*canvas.Image, error){ iconSize := getCustomFyneSize(0) if (identityIsActivated == false || balanceIsSufficient == false){ insufficientIcon, err := getFyneImageIcon("Insufficient") if (err != nil) { return nil, err } insufficientIcon.SetMinSize(iconSize) return insufficientIcon, nil } sufficientIcon, err := getFyneImageIcon("Sufficient") if (err != nil) { return nil, err } sufficientIcon.SetMinSize(iconSize) return sufficientIcon, nil } balanceStatusIcon, err := getBalanceStatusIcon() if (err != nil){ return nil, err } balanceStatusIconCentered := getFyneImageCentered(balanceStatusIcon) getBalanceStatusText := func()string{ if (balanceIsSufficient == true){ return "Sufficient" } return "Insufficient" } balanceStatusText := getBalanceStatusText() balanceStatusLabel := getBoldLabelCentered(balanceStatusText) displaySection := container.NewVBox(currentBalanceStatusText, balanceStatusIconCentered, balanceStatusLabel, widget.NewSeparator()) if (balanceIsSufficient == true){ timeRemainingTitle := getLabelCentered("Time Until Expiration:") currentTime := time.Now().Unix() if (currentTime > balanceExpirationTime){ return nil, errors.New("Balance expiration time is less than current time while Balance is sufficient = true") } timeLeft := balanceExpirationTime - currentTime timeTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(timeLeft, true) if (err != nil) { return nil, err } timeRemainingLabel := getBoldLabelCentered(timeTranslated) displaySection.Add(timeRemainingTitle) displaySection.Add(timeRemainingLabel) } refreshBalanceButton := widget.NewButtonWithIcon(translate("Refresh"), theme.ViewRefreshIcon(), func(){ //TODO: Add manualDownloads download and page to monitor it showUnderConstructionDialog(window) }) addTimeButton := widget.NewButtonWithIcon(translate("Add Time"), theme.MoveUpIcon(), func(){ setIncreaseMyIdentityBalancePage(window, myIdentityType, 30, currentPage) }) buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, refreshBalanceButton, addTimeButton)) displaySection.Add(buttonsGrid) return displaySection, nil } identityExpirationTimeSection, err := getMyIdentityExpirationTimeDisplaySection() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), identityExpirationTimeSection) setPageContent(page, window) }