package gui // chatGui.go implements pages for a user to view and sort their chat conversations, send chat messages, and view their chat statistics 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/allowedText" import "seekia/internal/appMemory" import "seekia/internal/encoding" import "seekia/internal/globalSettings" import "seekia/internal/helpers" import "seekia/internal/identity" import "seekia/internal/imagery" import "seekia/internal/messaging/myChatConversations" import "seekia/internal/messaging/myChatFilters" import "seekia/internal/messaging/myChatFilterStatistics" import "seekia/internal/messaging/myChatMessages" import "seekia/internal/messaging/myConversationIndexes" import "seekia/internal/messaging/myMessageQueue" import "seekia/internal/messaging/myReadStatus" import "seekia/internal/messaging/peerChatKeys" import "seekia/internal/messaging/readMessages" import "seekia/internal/messaging/sendMessages" import "seekia/internal/myBlockedUsers" import "seekia/internal/myContacts" import "seekia/internal/myIdentity" import "seekia/internal/mySettings" import "seekia/internal/network/appNetworkType/getAppNetworkType" import "seekia/internal/network/myAccountCredit" import "seekia/internal/parameters/getParameters" import "seekia/internal/profiles/attributeDisplay" import "seekia/internal/profiles/myLocalProfiles" import "seekia/internal/profiles/myProfileStatus" import "seekia/internal/profiles/profileStorage" import "seekia/internal/profiles/readProfiles" import "seekia/internal/profiles/viewableProfiles" import "time" import "image" import "errors" import "strings" import "slices" import "sync" //TODO: Page to prune Chat Messages, which allows deletion of messages older than X date func setChatPage(window fyne.Window){ setLoadingScreen(window, "Chat", "Loading chat page...") currentPage := func(){setChatPage(window)} appMemory.SetMemoryEntry("CurrentViewedPage", "Chat") title := getPageTitleCentered("Chat") getChatPageIdentityType := func()(string, error){ exists, myIdentityType, err := mySettings.GetSetting("ChatPageIdentityType") if (err != nil) { return "", err } if (exists == false){ return "Mate", nil } if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ return "", errors.New("Invalid ChatPageIdentityType: " + myIdentityType) } return myIdentityType, nil } myIdentityType, err := getChatPageIdentityType() if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } checkIfPageHasChangedFunction := func()(bool, error){ exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") if (exists == false || currentViewedPage != "Chat"){ return true, nil } exists, pageIdentityType, err := mySettings.GetSetting("ChatPageIdentityType") if (err != nil) { return false, err } if (exists == false){ if (myIdentityType == "Mate"){ return false, nil } return true, nil } if (myIdentityType != pageIdentityType){ return true, nil } return false, nil } creditIcon, err := getFyneImageIcon("Funds") if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } creditButton := widget.NewButton("Credit", func(){ setViewMyAccountCreditPage(window, myIdentityType, currentPage) }) creditButtonWithIcon := container.NewGridWithRows(2, creditIcon, creditButton) filtersIcon, err := getFyneImageIcon("Desires") if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } filtersButton := widget.NewButton("Filters", func(){ setMyChatFiltersPage(window, myIdentityType) }) filtersButtonWithIcon := container.NewGridWithRows(2, filtersIcon, filtersButton) statsIcon, err := getFyneImageIcon("Stats") if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } statsButton := widget.NewButton("Stats", func(){ setChatStatisticsPage(window, myIdentityType, currentPage) }) statsButtonWithIcon := container.NewGridWithRows(2, statsIcon, statsButton) contactsIcon, err := getFyneImageIcon("Contacts") if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } contactsButton := widget.NewButton("Contacts", func(){ setMyContactsPage(window, myIdentityType, currentPage) }) contactsButtonWithIcon := container.NewGridWithRows(2, contactsIcon, contactsButton) getIdentityTypeLabelOrChangeButton := func()(fyne.Widget, error){ getModeratorModeEnabledBool := func()(bool, error){ exists, moderatorModeOnOffStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus") if (err != nil) { return false, err } if (exists == true && moderatorModeOnOffStatus == "On"){ return true, nil } return false, nil } moderatorModeEnabled, err := getModeratorModeEnabledBool() if (err != nil) { return nil, err } moderatorIdentityExists, _, err := myIdentity.GetMyIdentityHash("Moderator") if (err != nil) { return nil, err } if (moderatorIdentityExists == false && moderatorModeEnabled == false){ mateLabel := getBoldLabel("Mate") return mateLabel, nil } getNextIdentityType := func()string{ if (myIdentityType == "Mate"){ return "Moderator" } return "Mate" } nextIdentityType := getNextIdentityType() changeIdentityTypeButton := widget.NewButton(myIdentityType, func(){ err := mySettings.SetSetting("ChatPageIdentityType", nextIdentityType) if (err != nil){ setErrorEncounteredPage(window, err, func(){setChatPage(window)}) return } currentPage() }) return changeIdentityTypeButton, nil } identityTypeLabelOrChangeButton, err := getIdentityTypeLabelOrChangeButton() if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } identityTypeIcon, err := getIdentityTypeIcon(myIdentityType, -20) if (err != nil) { setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } identityTypeLabelOrChangeButtonWithIcon := container.NewGridWithColumns(1, identityTypeIcon, identityTypeLabelOrChangeButton) pageButtonsRow := getContainerCentered(container.NewGridWithRows(1, creditButtonWithIcon, filtersButtonWithIcon, identityTypeLabelOrChangeButtonWithIcon, statsButtonWithIcon, contactsButtonWithIcon)) currentSortByAttribute, err := myChatConversations.GetConversationsSortByAttribute(myIdentityType) if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } sortingByLabel := getBoldLabel("Sorting By:") sortByAttributeTitle, _, formatSortByAttributeValuesFunction, sortByAttributeUnits, unknownSortByAttributeText, err := attributeDisplay.GetProfileAttributeDisplayInfo(currentSortByAttribute) if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } sortByButton := widget.NewButton(sortByAttributeTitle, func(){ setSelectMyConversationsSortByAttributePage(window, myIdentityType, currentPage) }) getSortDirectionButtonWithIcon := func()(fyne.Widget, error){ currentSortDirection, err := myChatConversations.GetConversationsSortDirection(myIdentityType) if (err != nil) { return nil, err } if (currentSortDirection == "Ascending"){ button := widget.NewButtonWithIcon(translate("Ascending"), theme.MoveUpIcon(), func(){ appMemory.SetMemoryEntry("StopConversationsGenerationYesNo", "Yes") _ = mySettings.SetSetting(myIdentityType + "ChatConversations_SortDirection", "Descending") _ = mySettings.SetSetting(myIdentityType + "ChatConversationsSortedStatus", "No") _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") currentPage() }) return button, nil } button := widget.NewButtonWithIcon(translate("Descending"), theme.MoveDownIcon(), func(){ appMemory.SetMemoryEntry("StopConversationsGenerationYesNo", "Yes") _ = mySettings.SetSetting(myIdentityType + "ChatConversations_SortDirection", "Ascending") _ = mySettings.SetSetting(myIdentityType + "ChatConversationsSortedStatus", "No") _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") currentPage() }) return button, nil } sortByDirectionButton, err := getSortDirectionButtonWithIcon() if (err != nil) { setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } sortByRow := container.NewHBox(layout.NewSpacer(), sortingByLabel, sortByButton, sortByDirectionButton, layout.NewSpacer()) appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } messagesReady, err := myChatConversations.GetMyChatConversationsReadyStatus(myIdentityType, appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } if (messagesReady == false) { progressPercentageBinding := binding.NewFloat() progressDescriptionBinding := binding.NewString() updateConversationsAndLoadingBarFunction := func(){ err = myChatConversations.StartUpdatingMyConversations(myIdentityType, appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, currentPage) return } var encounteredError error for{ pageHasChanged, err := checkIfPageHasChangedFunction() if (err != nil){ //TODO: Log error in logger return } if (pageHasChanged == true){ appMemory.SetMemoryEntry("StopBuildMyConversationsYesNo", "Yes") return } buildEncounteredError, errorEncounteredString, buildIsStopped, conversationsAreReady, currentPercentageProgress, err := myChatConversations.GetChatConversationsBuildStatus(myIdentityType, appNetworkType) if (err != nil){ encounteredError = err break } if (buildEncounteredError == true){ encounteredError = errors.New(errorEncounteredString) break } if (buildIsStopped == true) { return } if (conversationsAreReady == true){ progressPercentageBinding.Set(1) // We wait so that the loading bar will appear complete. time.Sleep(100 * time.Millisecond) setChatPage(window) return } progressPercentageBinding.Set(currentPercentageProgress) if (currentPercentageProgress >= 0.50){ progressDescriptionBinding.Set("Sorting Conversations...") } time.Sleep(100 * time.Millisecond) } // This should only be reached if an error is encountered errorToShow := errors.New("Error encountered while generating conversations: " + encounteredError.Error()) setErrorEncounteredPage(window, errorToShow, currentPage) } loadingLabel := getBoldLabelCentered("Loading conversations...") loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding)) loadingDetailsLabel := widget.NewLabelWithData(progressDescriptionBinding) loadingDetailsLabel.TextStyle = getFyneTextStyle_Italic() loadingDetailsLabelCentered := getWidgetCentered(loadingDetailsLabel) page := container.NewVBox(title, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), loadingLabel, loadingBar, loadingDetailsLabelCentered) setPageContent(page, window) go updateConversationsAndLoadingBarFunction() return } getConversationsContainer := func()(*fyne.Container, error){ conversationsAreReady, conversationsList, err := myChatConversations.GetMyChatConversationsMapList(myIdentityType, appNetworkType) if (err != nil) { return nil, err } if (conversationsAreReady == false){ return nil, errors.New("Chat conversations not ready after being ready already.") } getRefreshResultsButtonText := func()(string, error){ needsRefresh, err := myChatConversations.CheckIfMyChatConversationsNeedRefresh(myIdentityType) if (err != nil) { return "", err } if (needsRefresh == false){ return "Refresh Results", nil } return "Refresh Results - Updates Available!", nil } refreshButtonText, err := getRefreshResultsButtonText() if (err != nil){ return nil, err } refreshResultsButton := getWidgetCentered(widget.NewButtonWithIcon(refreshButtonText, theme.ViewRefreshIcon(), func(){ _ = mySettings.SetSetting(myIdentityType + "ChatMessagesReadyStatus", "No") _ = mySettings.SetSetting(myIdentityType + "ChatConversationsGeneratedStatus", "No") _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") currentPage() })) numberOfConversations := len(conversationsList) numberOfConversationsString := helpers.ConvertIntToString(numberOfConversations) if (numberOfConversations == 0) { lowercaseIdentityType := strings.ToLower(myIdentityType) noConversationsFoundText := getBoldLabelCentered("No " + lowercaseIdentityType + " conversations found.") numberOfFilters, err := myChatFilters.GetNumberOfEnabledChatFilters(myIdentityType) if (err != nil) { return nil, err } if (numberOfFilters == 0){ noConversationsFoundLabelWithRefreshButton := container.NewVBox(noConversationsFoundText, refreshResultsButton) return noConversationsFoundLabelWithRefreshButton, nil } numberOfFiltersString := helpers.ConvertIntToString(numberOfFilters) getActiveFiltersText := func()string{ if (numberOfFilters == 1){ return "active filter" } return "active filters" } activeFiltersText := getActiveFiltersText() activeFiltersLabel := getItalicLabelCentered(numberOfFiltersString + " " + activeFiltersText) noConversationsFoundTextWithFilters := container.NewVBox(noConversationsFoundText, activeFiltersLabel, refreshResultsButton) return noConversationsFoundTextWithFilters, nil } getViewIndex := func()(int, error){ exists, viewIndexString, err := mySettings.GetSetting(myIdentityType + "ChatConversations_ViewIndex") if (err != nil) { return 0, err } if (exists == false){ return 0, nil } viewIndex, err := helpers.ConvertStringToInt(viewIndexString) if (err != nil){ return 0, errors.New("Invalid chat conversations view index: " + viewIndexString) } if (viewIndex < 0) { return 0, nil } maximumViewIndex := numberOfConversations-1 if (viewIndex > maximumViewIndex){ return maximumViewIndex, nil } return viewIndex, nil } viewIndex, err := getViewIndex() if (err != nil) { return nil, err } getNavigateToBeginningButton := func()fyne.Widget{ if (numberOfConversations <= 5 || viewIndex == 0){ emptyButton := widget.NewButton(" ", nil) return emptyButton } goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){ mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", "0") currentPage() }) return goToBeginningButton } getNavigateToEndButton := func()fyne.Widget{ emptyButton := widget.NewButton(" ", nil) if (numberOfConversations <= 5){ return emptyButton } finalPageMinimumIndex := numberOfConversations - 5 if (viewIndex >= finalPageMinimumIndex){ return emptyButton } goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){ finalPageIndexString := helpers.ConvertIntToString(finalPageMinimumIndex) _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", finalPageIndexString) currentPage() }) return goToEndButton } getNavigateLeftButton := func()fyne.Widget{ if (numberOfConversations <= 5 || viewIndex == 0){ emptyButton := widget.NewButton(" ", nil) return emptyButton } button := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ newIndex := helpers.ConvertIntToString(viewIndex-5) _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", newIndex) currentPage() }) return button } getNavigateRightButton := func()fyne.Widget{ emptyButton := widget.NewButton(" ", nil) if (numberOfConversations <= 5){ return emptyButton } finalPageMinimumIndex := numberOfConversations - 5 if (viewIndex >= finalPageMinimumIndex){ return emptyButton } button := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ newIndex := helpers.ConvertIntToString(viewIndex+5) _ = mySettings.SetSetting(myIdentityType + "ChatConversations_ViewIndex", newIndex) currentPage() }) return button } getViewingConversationsRow := func()*fyne.Container{ getConversationOrConversationsText := func()string{ if (numberOfConversationsString == "1"){ return "Conversation" } return "Conversations" } conversationOrConversationsText := getConversationOrConversationsText() viewingConversationsText := getBoldLabel("Viewing " + numberOfConversationsString + " " + conversationOrConversationsText) if (numberOfConversations <= 5){ viewingConversationsRow := getWidgetCentered(viewingConversationsText) return viewingConversationsRow } navigateToBeginningButton := getNavigateToBeginningButton() navigateToEndButton := getNavigateToEndButton() navigateLeftButton := getNavigateLeftButton() navigateRightButton := getNavigateRightButton() viewingConversationsRow := container.NewHBox(layout.NewSpacer(), navigateToBeginningButton, navigateLeftButton, viewingConversationsText, navigateRightButton, navigateToEndButton, layout.NewSpacer()) return viewingConversationsRow } viewingConversationsRow := getViewingConversationsRow() viewIndexOnwardsConversationsList := conversationsList[viewIndex:] conversationsContainer := container.NewVBox() if (viewIndex == 0){ conversationsContainer.Add(refreshResultsButton) conversationsContainer.Add(widget.NewSeparator()) } for index, conversationMap := range viewIndexOnwardsConversationsList{ resultIndex := viewIndex + index + 1 resultIndexString := helpers.ConvertIntToString(resultIndex) myIdentityHashString, exists := conversationMap["MyIdentityHash"] if (exists == false) { return nil, errors.New("Malformed conversation map: Missing MyIdentityHash") } theirIdentityHashString, exists := conversationMap["TheirIdentityHash"] if (exists == false) { return nil, errors.New("Malformed conversation map: Missing TheirIdentityHash") } myIdentityHash, _, err := identity.ReadIdentityHashString(myIdentityHashString) if (err != nil){ return nil, errors.New("Malformed conversation map: Contains invalid MyIdentityHash: " + myIdentityHashString) } theirIdentityHash, _, err := identity.ReadIdentityHashString(theirIdentityHashString) if (err != nil){ return nil, errors.New("Malformed conversation map: Contains invalid TheirIdentityHash: " + theirIdentityHashString) } getAllowUnknownViewableStatusBool := func()bool{ if (myIdentityType == "Mate"){ return false } return true } allowUnknownViewableStatusBool := getAllowUnknownViewableStatusBool() theirProfileExists, _, getAnyAttributeFromTheirProfileFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(theirIdentityHash, appNetworkType, true, allowUnknownViewableStatusBool, true) if (err != nil) { return nil, err } getAvatarOrImage := func()(image.Image, error){ if (theirProfileExists == true){ attributeExists, _, photosAttributeValue, err := getAnyAttributeFromTheirProfileFunction("Photos") if (err != nil) { return nil, err } if (attributeExists == true){ base64PhotosList := strings.Split(photosAttributeValue, "+") firstPhotoBase64 := base64PhotosList[0] userImageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(firstPhotoBase64) if (err != nil) { return nil, errors.New("Database corrupt: Contains profile with invalid photos attribute.") } return userImageObject, nil } } getContactEmojiIdentifier := func()(int, error){ if (theirProfileExists == false){ return 2929, nil } attributeExists, _, avatarAttributeValue, err := getAnyAttributeFromTheirProfileFunction("Avatar") if (err != nil) { return 0, err } if (attributeExists == false){ return 2929, nil } userEmojiIdentifier, err := helpers.ConvertStringToInt(avatarAttributeValue) if (err != nil) { return 0, errors.New("Database corrupt: Contains profile with invalid emoji attribute: " + avatarAttributeValue) } return userEmojiIdentifier, nil } contactEmojiIdentifier, err := getContactEmojiIdentifier() if (err != nil) { return nil, err } emojiImageObject, err := getEmojiImageObject(contactEmojiIdentifier) if (err != nil) { return nil, err } return emojiImageObject, nil } getSortByAttributeBox := func()(*fyne.Container, error){ sortByAttributeTitleLabel := getLabelCentered(sortByAttributeTitle) getSortByAttributeValueText := func()(string, error){ if (theirProfileExists == false){ return unknownSortByAttributeText, nil } exists, _, attributeValue, err := getAnyAttributeFromTheirProfileFunction(currentSortByAttribute) if (err != nil) { return "", err } if (exists == false){ return unknownSortByAttributeText, nil } attributeValueFormatted, err := formatSortByAttributeValuesFunction(attributeValue) if (err != nil) { return "", err } result := attributeValueFormatted + sortByAttributeUnits return result, nil } sortByAttributeValueText, err := getSortByAttributeValueText() if (err != nil) { return nil, err } attributeValueLabel := getBoldLabelCentered(sortByAttributeValueText) sortByAttributeBox := getContainerBoxed(container.NewVBox(sortByAttributeTitleLabel, attributeValueLabel)) return sortByAttributeBox, nil } getReadUnreadStatusButton := func()(fyne.Widget, error){ conversationReadUnreadStatus, err := myReadStatus.GetConversationReadUnreadStatus(myIdentityHash, theirIdentityHash, appNetworkType) if (err != nil) { return nil, err } if (conversationReadUnreadStatus == "Unread"){ unreadStatusButton := widget.NewButtonWithIcon("Unread", theme.WarningIcon(), func(){ dialogTitle := translate("Conversation Is Unread") dialogMessageA := getLabelCentered(translate("This conversation is unread.")) dialogMessageB := getLabelCentered(translate("It contains new messages for you to read.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) }) unreadStatusButton.Importance = widget.HighImportance return unreadStatusButton, nil } readStatusButton := widget.NewButtonWithIcon("Read", theme.VisibilityIcon(), func(){ dialogTitle := translate("Conversation Is Read") dialogMessageA := getLabelCentered(translate("This conversation is read.")) dialogMessageB := getLabelCentered(translate("It does not contain any new messages for you to read.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) }) return readStatusButton, nil } getUserName := func()(string, error){ if (theirProfileExists == false){ // We haven't downloaded their profile yet // Their profile may not exist on the network return "Unknown", nil } exists, _, username, err := getAnyAttributeFromTheirProfileFunction("Username") if (err != nil) { return "", err } if (exists == false) { return "Anonymous", nil } theirUsernameTrimmed, _, err := helpers.TrimAndFlattenString(username, 15) if (err != nil) { return "", err } return theirUsernameTrimmed, nil } avatarOrImage, err := getAvatarOrImage() if (err != nil) { return nil, err } userFyneImage := canvas.NewImageFromImage(avatarOrImage) userFyneImage.FillMode = canvas.ImageFillContain imageSize := getCustomFyneSize(10) userFyneImage.SetMinSize(imageSize) userImageBoxed := getFyneImageBoxed(userFyneImage) currentTheirUsername, err := getUserName() if (err != nil) { return nil, err } userNameLabel := getBoldLabelCentered(currentTheirUsername) theirIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(theirIdentityHashString, 15) if (err != nil) { return nil, err } theirIdentityHashLabel := widget.NewLabel(theirIdentityHashTrimmed) userNameColumn := getContainerBoxed(container.NewVBox(userNameLabel, theirIdentityHashLabel)) sortByAttributeBox, err := getSortByAttributeBox() if (err != nil) { return nil, err } readUnreadStatusButton, err := getReadUnreadStatusButton() if (err != nil) { return nil, err } chatButton := widget.NewButtonWithIcon("Chat", theme.MailComposeIcon(), func(){ setViewAConversationPage(window, theirIdentityHash, true, currentPage) }) resultIndexBoldLabel := getBoldLabel(resultIndexString + ".") conversationRow := container.NewHBox(layout.NewSpacer(), resultIndexBoldLabel, userImageBoxed, userNameColumn, sortByAttributeBox, readUnreadStatusButton, chatButton, layout.NewSpacer()) conversationsContainer.Add(conversationRow) if (index >= 4) { break } conversationsContainer.Add(widget.NewSeparator()) } conversationsContainerScrollable := container.NewVScroll(conversationsContainer) conversationsContainerBoxed := getWidgetBoxed(conversationsContainerScrollable) resultsContainer := container.NewBorder(viewingConversationsRow, nil, nil, nil, conversationsContainerBoxed) return resultsContainer, nil } conversationsContent, err := getConversationsContainer() if (err != nil){ setErrorEncounteredPage(window, err, func(){setHomePage(window)}) return } pageHeader := container.NewVBox(title, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator()) content := container.NewBorder(pageHeader, nil, nil, nil, conversationsContent) setPageContent(content, window) } func setSelectMyConversationsSortByAttributePage(window fyne.Window, identityType string, previousPage func()){ appMemory.SetMemoryEntry("CurrentViewedPage", "SortBySelectPage_Chat") title := getPageTitleCentered("Select Sort By Attribute") backButton := getBackButtonCentered(previousPage) description := getLabelCentered("Select the attribute to sort your chat conversations by.") getPageContent := func()(*fyne.Container, error){ if (identityType == "Mate"){ generalAttributeButtonsGrid := container.NewGridWithColumns(1) physicalAttributeButtonsGrid := container.NewGridWithColumns(1) lifestyleAttributeButtonsGrid := container.NewGridWithColumns(1) mentalAttributeButtonsGrid := container.NewGridWithColumns(1) addAttributeSelectButton := func(attributeType string, attributeName string, sortDirection string)error{ attributeTitle, _, _, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeName) if (err != nil) { return err } attributeButton := widget.NewButton(attributeTitle, func(){ _ = mySettings.SetSetting(identityType + "ChatConversationsSortedStatus", "No") _ = mySettings.SetSetting(identityType + "ChatConversations_SortByAttribute", attributeName) _ = mySettings.SetSetting(identityType + "ChatConversations_SortDirection", sortDirection) _ = mySettings.SetSetting(identityType + "ChatConversations_ViewIndex", "0") previousPage() }) if (attributeType == "General"){ generalAttributeButtonsGrid.Add(attributeButton) } else if (attributeType == "Physical"){ physicalAttributeButtonsGrid.Add(attributeButton) } else if (attributeType == "Lifestyle"){ lifestyleAttributeButtonsGrid.Add(attributeButton) } else if (attributeType == "Mental"){ mentalAttributeButtonsGrid.Add(attributeButton) } else { return errors.New("addSelectButton called with invalid attributeType: " + attributeType) } return nil } generalLabel := getBoldLabelCentered("General") err := addAttributeSelectButton("General", "MatchScore", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("General", "Distance", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("General", "SearchTermsCount", "Descending") if (err != nil) { return nil, err } physicalLabel := getBoldLabelCentered("Physical") err = addAttributeSelectButton("Physical", "Age", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "Height", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "BodyFat", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "BodyMuscle", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "SkinColor", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "HairTexture", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "RacialSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "EyeColorSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "EyeColorGenesSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "HairColorSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "HairColorGenesSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "SkinColorSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "SkinColorGenesSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "HairTextureSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "HairTextureGenesSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "FacialStructureGenesSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "23andMe_AncestralSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "23andMe_MaternalHaplogroupSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "23andMe_PaternalHaplogroupSimilarity", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "OffspringProbabilityOfAnyMonogenicDisease", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "TotalPolygenicDiseaseRiskScore", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Physical", "OffspringTotalPolygenicDiseaseRiskScore", "Ascending") if (err != nil) { return nil, err } offspringLactoseToleranceProbabilityButton := widget.NewButton("Offspring Lactose Tolerance Probability", func(){ //TODO showUnderConstructionDialog(window) }) physicalAttributeButtonsGrid.Add(offspringLactoseToleranceProbabilityButton) offspringCurlyHairProbabilityButton := widget.NewButton("Offspring Curly Hair Probability", func(){ //TODO showUnderConstructionDialog(window) }) physicalAttributeButtonsGrid.Add(offspringCurlyHairProbabilityButton) offspringStraightHairProbabilityButton := widget.NewButton("Offspring Straight Hair Probability", func(){ //TODO showUnderConstructionDialog(window) }) physicalAttributeButtonsGrid.Add(offspringStraightHairProbabilityButton) err = addAttributeSelectButton("Physical", "23andMe_NeanderthalVariants", "Descending") if (err != nil) { return nil, err } lifestyleLabel := getBoldLabelCentered("Lifestyle") err = addAttributeSelectButton("Lifestyle", "WealthInGold", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "Fame", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "FruitRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "VegetablesRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "NutsRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "GrainsRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "DairyRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "SeafoodRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "BeefRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "PorkRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "PoultryRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "EggsRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "BeansRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "AlcoholFrequency", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "TobaccoFrequency", "Ascending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Lifestyle", "CannabisFrequency", "Ascending") if (err != nil) { return nil, err } mentalLabel := getBoldLabelCentered("Mental") err = addAttributeSelectButton("Mental", "PetsRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Mental", "CatsRating", "Descending") if (err != nil) { return nil, err } err = addAttributeSelectButton("Mental", "DogsRating", "Descending") if (err != nil) { return nil, err } generalAttributeButtonsGridCentered := getContainerCentered(generalAttributeButtonsGrid) physicalAttributeButtonsGridCentered := getContainerCentered(physicalAttributeButtonsGrid) lifestyleAttributeButtonsGridCentered := getContainerCentered(lifestyleAttributeButtonsGrid) mentalAttributeButtonsGridCentered := getContainerCentered(mentalAttributeButtonsGrid) pageContent := container.NewVBox(generalLabel, widget.NewSeparator(), generalAttributeButtonsGridCentered, widget.NewSeparator(), physicalLabel, widget.NewSeparator(), physicalAttributeButtonsGridCentered, widget.NewSeparator(), lifestyleLabel, widget.NewSeparator(), lifestyleAttributeButtonsGridCentered, widget.NewSeparator(), mentalLabel, widget.NewSeparator(), mentalAttributeButtonsGridCentered) return pageContent, nil } else if (identityType == "Moderator"){ getSelectButton := func(attributeTitle string, attributeName string, sortDirection string) fyne.Widget{ button := widget.NewButton(translate(attributeTitle), func(){ _ = mySettings.SetSetting("ModeratorChatConversationsSortedStatus", "No") _ = mySettings.SetSetting("ModeratorChatConversations_SortByAttribute", attributeName) _ = mySettings.SetSetting("ModeratorChatConversations_SortDirection", sortDirection) _ = mySettings.SetSetting("ModeratorChatConversations_ViewIndex", "0") previousPage() }) return button } identityScoreButton := getSelectButton("Identity Score", "IdentityScore", "Descending") buttonsGrid := container.NewGridWithColumns(1, identityScoreButton) pageContent := getContainerCentered(buttonsGrid) return pageContent, nil } return nil, errors.New("setSelectMyConversationsSortByAttributePage called with invalid identityType: " + identityType) } pageContent, err := getPageContent() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), pageContent) setPageContent(page, window) } func setMyChatFiltersPage(window fyne.Window, identityType string){ appMemory.SetMemoryEntry("CurrentViewedPage", "ChatFilters") title := getPageTitleCentered(identityType + " Chat Filters") previousPage := func(){setChatPage(window)} backButton := getBackButtonCentered(previousPage) filtersDescription := getLabelCentered("Choose your chat conversation filters.") getChatFiltersGrid := func()(*fyne.Container, error){ filterDescriptionsColumn := container.NewVBox() filterChecksColumn := container.NewVBox() addChatFilterRow := func(chatFilterDescription string, chatFilterName string)error{ currentStatus, err := myChatFilters.GetChatFilterOnOffStatus(identityType, chatFilterName) if (err != nil) { return err } chatFilterDescriptionLabel := getBoldLabelCentered(chatFilterDescription) chatFilterCheck := widget.NewCheck("", func(response bool){ err := myChatFilters.SetChatFilterOnOffStatus(identityType, chatFilterName, response) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) } }) if (currentStatus == true){ chatFilterCheck.Checked = true } filterDescriptionsColumn.Add(chatFilterDescriptionLabel) filterChecksColumn.Add(chatFilterCheck) filterDescriptionsColumn.Add(widget.NewSeparator()) filterChecksColumn.Add(widget.NewSeparator()) return nil } err := addChatFilterRow("Only show conversations with my contacts.", "ShowMyContactsOnly") if (err != nil){ return nil, err } if (identityType == "Mate"){ err := addChatFilterRow("Only show conversations with my matches.", "ShowMyMatchesOnly") if (err != nil){ return nil, err } } err = addChatFilterRow("Only show conversations with users who have messaged me.", "ShowHasMessagedMeOnly") if (err != nil){ return nil, err } if (identityType == "Mate"){ err = addChatFilterRow("Only show conversations with users I have liked.", "OnlyShowLikedUsers") if (err != nil) { return nil, err } err = addChatFilterRow("Hide conversations with users I have ignored.", "HideIgnoredUsers") if (err != nil) { return nil, err } } //TODO: Hide conversations with users who are banned chatFiltersGrid := container.NewHBox(layout.NewSpacer(), filterDescriptionsColumn, filterChecksColumn, layout.NewSpacer()) return chatFiltersGrid, nil } chatFiltersGrid, err := getChatFiltersGrid() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } page := container.NewVBox(title, backButton, widget.NewSeparator(), filtersDescription, widget.NewSeparator(), chatFiltersGrid) setPageContent(page, window) } func setViewAConversationPage(window fyne.Window, theirIdentityHash [16]byte, resetConversationIndex bool, previousPage func()){ setLoadingScreen(window, "View Conversation", "Loading conversation...") currentPage := func(){ setViewAConversationPage(window, theirIdentityHash, false, previousPage) } currentPageWithNewestView := func(){setViewAConversationPage(window, theirIdentityHash, true, previousPage)} title := getPageTitleCentered("Viewing Conversation") backButton := getBackButtonCentered(previousPage) theirIdentityHashString, theirIdentityType, err := identity.EncodeIdentityHashBytesToString(theirIdentityHash) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (theirIdentityType == "Host"){ title := getPageTitleCentered("Chat") description1 := getBoldLabelCentered("Recipient is a Host profile.") description2 := getLabelCentered("They cannot be chatted with.") page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) setPageContent(page, window) return } myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(theirIdentityType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityExists == false){ // This should not happen, because conversations are generated from user's existing identitites. // A user's chat conversations should be regenerated whenever a user deletes/changes their identity err := mySettings.SetSetting(theirIdentityType + "ChatConversationsGeneratedStatus", "No") if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } title := getPageTitleCentered("Create Chat Identity") description1 := getBoldLabelCentered("Recipient " + theirIdentityHashString + " is a " + theirIdentityType + " identity.") description2 := getLabelCentered("You do not have a " + theirIdentityType + " identity.") description3 := getLabelCentered("Create your " + theirIdentityType + " identity to chat from?") createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create " + theirIdentityType + " Identity", theme.NavigateNextIcon(), func(){ setChooseNewIdentityHashPage(window, theirIdentityType, currentPage, currentPage) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, createIdentityButton) setPageContent(page, window) return } if (myIdentityHash == theirIdentityHash){ // This should not happen setErrorEncounteredPage(window, errors.New("Trying to chat with myself."), previousPage) return } myIdentityType := theirIdentityType appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } getTheyAreDisabledStatus := func()(bool, error){ theirProfileExists, _, _, _, _, theirNewestProfileRawMap, err := profileStorage.GetNewestUserProfile(theirIdentityHash, appNetworkType) if (err != nil) { return false, err } if (theirProfileExists == false){ return false, nil } theyAreDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(theirNewestProfileRawMap, "Disabled") if (err != nil) { return false, err } if (theyAreDisabled == true){ return true, nil } return false, nil } theyAreDisabled, err := getTheyAreDisabledStatus() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } getIAmDisabledStatus := func()(bool, error){ exists, iAmDisabled, err := myLocalProfiles.GetProfileData(myIdentityType, "Disabled") if (err != nil) { return false, err } if (exists == true && iAmDisabled == "Yes"){ return true, nil } return false, nil } iAmDisabled, err := getIAmDisabledStatus() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } getRecipientInfoColumn := func()(*fyne.Container, error){ chattingWithTitle := getBoldLabelCentered("Chatting With:") getRecipientUserName := func()(string, error){ theirProfileExists, _, _, _, _, theirRawProfileMap, err := profileStorage.GetNewestUserProfile(theirIdentityHash, appNetworkType) if (err != nil) { return "", err } if (theirProfileExists == false){ result := translate("Unknown") return result, nil } exists, username, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(theirRawProfileMap, "Username") if (err != nil) { return "", err } if (exists == false) { result := translate("Anonymous") return result, nil } trimmedUsername, _, err := helpers.TrimAndFlattenString(username, 15) if (err != nil) { return "", err } return trimmedUsername, nil } recipientUserName, err := getRecipientUserName() if (err != nil) { return nil, err } recipientUsernameLabel := getBoldItalicLabelCentered(recipientUserName) trimmedIdentityHash, _, err := helpers.TrimAndFlattenString(theirIdentityHashString, 15) if (err != nil) { return nil, err } theirIdentityHashLabel := getLabelCentered(trimmedIdentityHash) getViewTheirProfileButton := func()(*fyne.Container, error){ if (theyAreDisabled == true){ disabledButton := getWidgetCentered(widget.NewButtonWithIcon("Disabled", theme.VisibilityOffIcon(), func(){ dialogTitle := translate("User Is Disabled") dialogMessageA := getLabelCentered(translate("This user has disabled their profile.")) dialogMessageB := getLabelCentered(translate("They have no profile to view.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) })) return disabledButton, nil } viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ setViewPeerProfilePageFromIdentityHash(window, theirIdentityHash, currentPage) })) return viewProfileButton, nil } viewRecipientProfileButton, err := getViewTheirProfileButton() if (err != nil) { return nil, err } getMyContactSection := func()(*fyne.Container, error){ // Will show add to my contacts if not a contact // will show contact name if is already a contact contactExists, contactName, _, _, _, err := myContacts.GetMyContactDetails(theirIdentityHash) if (err != nil) { return nil, err } if (contactExists == false){ addContactButton := container.NewVBox(widget.NewButtonWithIcon("Add Contact", theme.ContentAddIcon(), func(){ setAddContactFromIdentityHashPage(window, theirIdentityHash, currentPage, currentPage) })) return addContactButton, nil } trimmedContactName, _, err := helpers.TrimAndFlattenString(contactName, 15) if (err != nil) { return nil, err } contactNameLabel := getItalicLabelCentered("Contact Name:") contactNameText := getWidgetCentered(getBoldItalicLabel(trimmedContactName)) contactSection := container.NewVBox(contactNameLabel, contactNameText) return contactSection, nil } myContactSection, err := getMyContactSection() if (err != nil) { return nil, err } actionsButton := widget.NewButtonWithIcon("Actions", theme.ContentRedoIcon(), func(){ setViewPeerActionsPage(window, theirIdentityHash, currentPage) }) recipientInfoColumn := getContainerBoxed(container.NewVBox(chattingWithTitle, widget.NewSeparator(), recipientUsernameLabel, theirIdentityHashLabel, viewRecipientProfileButton, widget.NewSeparator(), myContactSection, widget.NewSeparator(), actionsButton)) return recipientInfoColumn, nil } recipientInfoColumn, err := getRecipientInfoColumn() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } getMyInfoColumn := func()(*fyne.Container, error){ myIdentityLabel := getBoldLabelCentered("My Identity:") getMyUserName := func()(string, error){ exists, username, err := myLocalProfiles.GetProfileData(myIdentityType, "Username") if (err != nil) { return "", err } if (exists == false) { return "Anonymous", nil } trimmedUsername, _, err := helpers.TrimAndFlattenString(username, 15) if (err != nil) { return "", err } return trimmedUsername, nil } myUserName, err := getMyUserName() if (err != nil) { return nil, err } myUsernameLabel := getWidgetCentered(getBoldItalicLabel(myUserName)) myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash) if (err != nil){ return nil, err } trimmedIdentityHash, _, err := helpers.TrimAndFlattenString(myIdentityHashString, 15) if (err != nil) { return nil, err } myIdentityHashLabel := getLabelCentered(trimmedIdentityHash) getViewMyProfileButton := func()(*fyne.Container, error){ if (iAmDisabled == true){ disabledButton := getWidgetCentered(widget.NewButtonWithIcon("Disabled", theme.VisibilityOffIcon(), func(){ title := translate("Your Profile Is Disabled") dialogMessageA := getLabelCentered(translate("You have disabled your " + myIdentityType + " profile.")) dialogMessageB := getLabelCentered(translate("You have no profile to view.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(title, translate("Close"), dialogContent, window) })) return disabledButton, nil } viewProfileButton := getWidgetCentered(widget.NewButtonWithIcon("View Profile", theme.VisibilityIcon(), func(){ setViewMyProfilePage(window, myIdentityType, "Public", currentPage) })) return viewProfileButton, nil } viewMyProfileButton, err := getViewMyProfileButton() if (err != nil) { return nil, err } myIdentityTypeIcon, err := getIdentityTypeIcon(myIdentityType, 0) if (err != nil) { return nil, err } myIdentityTypeIconCentered := getFyneImageCentered(myIdentityTypeIcon) myIdentityTypeLabel := getBoldLabelCentered(myIdentityType) myInfoColumn := getContainerBoxed(container.NewVBox(myIdentityLabel, widget.NewSeparator(), myUsernameLabel, myIdentityHashLabel, viewMyProfileButton, widget.NewSeparator(), myIdentityTypeIconCentered, myIdentityTypeLabel)) return myInfoColumn, nil } myInfoColumn, err := getMyInfoColumn() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } getConversationContainer := func()(*fyne.Container, error){ myIdentityFound, conversationExists, messagesList, _, _, iHaveRejectedThem, _, _, theyHaveRejectedMe, _, err := myChatMessages.GetMyConversationInfoAndSortedMessagesList(myIdentityHash, theirIdentityHash, appNetworkType) if (err != nil) { return nil, err } if (myIdentityFound == false){ return nil, errors.New("My identity not found after being found already.") } // We use this list to store the non seen indicating messages // Seen messages are messages that are used to indicate that a user has seen another user's message // They are not shown to the user as messages, but are instead are shown as a color status next to each message nonSeenIndicatingMessagesList := make([]myChatMessages.ConversationMessage, 0) // We have to split seen indicating messages into two maps: Ones which I have seen, and ones which they have seen // Otherwise, a malicious conversator could make us falsely believe // we had sent a seen indicating message for one of the messages they sent us. // // This map stores the message hashes which have been seen by me mySeenMessagesMap := make(map[[26]byte]struct{}) // This map stores the message hashes which they have seen theirSeenMessagesMap := make(map[[26]byte]struct{}) for _, messageObject := range messagesList{ currentMessageContent := messageObject.Communication seenMessageHashHex, isSeenMessage := strings.CutPrefix(currentMessageContent, ">!>Seen=") if (isSeenMessage == false){ // This message is not a seen indicating message nonSeenIndicatingMessagesList = append(nonSeenIndicatingMessagesList, messageObject) continue } seenMessageHash, err := readMessages.ReadMessageHashHex(seenMessageHashHex) if (err != nil){ // This should not happen // Seekia should prevent anyone from sending a message with this prefix that does not contain a valid message hash // The myChatMessages package should also reject any invalid messages from being added. return nil, errors.New("GetMyConversationInfoAndSortedMessagesList returning message which contains invalid seen indicating message: " + seenMessageHashHex) } currentMessageIAmSender := messageObject.IAmSender if (currentMessageIAmSender == true){ mySeenMessagesMap[seenMessageHash] = struct{}{} } else { theirSeenMessagesMap[seenMessageHash] = struct{}{} } } // We use this function to determine if a message in the conversation has been seen by the user it was sent to // Inputs: // -bool: This is the IAmSender for the message which we are trying to get the Seen status of // -[26]byte: This is the message hash we are trying to get the Seen status of // Output: // -bool: Message is seen getMessageIsSeenStatus := func(iAmSender bool, messageHash [26]byte)bool{ if (iAmSender == true){ _, exists := theirSeenMessagesMap[messageHash] return exists } _, exists := mySeenMessagesMap[messageHash] return exists } // Conversation view index is the index of the first message to display (ignoring seen-indicating messages) // Will show the next 5 messages after the view index, unless less than 5 messages exist, in which case it will show whatever is left // If more than five messages exist, the first page will show the remainder of messages, which may not be 5, because every other page displays exactly 5 messages //TODO: Conversation view index should actually be calculated from a message hash/message identifier // Otherwise, new messages will shift the current conversation view index // For example, 5 messages are sent, so the conversation view index is now shifted by 1 page numberOfMessages := len(nonSeenIndicatingMessagesList) getMaximumViewIndex := func()int{ if (numberOfMessages <= 5){ return 0 } // Maximum view index = index of first message for the newest page of messages maximumViewIndex := numberOfMessages - 5 return maximumViewIndex } maximumViewIndex := getMaximumViewIndex() getConversationViewIndex := func()(int, error){ // We show 5 messages per page if (numberOfMessages <= 5){ return 0, nil } cacheConversationIndexExists, cacheConversationIndex, err := myConversationIndexes.GetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType) if (err != nil){ return 0, err } if (cacheConversationIndexExists == false || cacheConversationIndex > maximumViewIndex || resetConversationIndex == true){ err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, maximumViewIndex) if (err != nil) { return 0, err } return maximumViewIndex, nil } if (cacheConversationIndex <= 0){ return 0, nil } return cacheConversationIndex, nil } conversationViewIndex, err := getConversationViewIndex() if (err != nil) { return nil, err } err = myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, conversationViewIndex) if (err != nil) { return nil, err } // Last Message Index is the index of the last message on display getLastMessageIndex := func()int{ if (numberOfMessages == 0){ return 0 } if (numberOfMessages <= 5){ finalMessageIndex := numberOfMessages-1 return finalMessageIndex } if (conversationViewIndex == 0){ // First page will not show next five messages // It will show the remainder of messages, because all other pages show 5 messages each dividedFiveRemainder := numberOfMessages % 5 if (dividedFiveRemainder != 0){ return dividedFiveRemainder - 1 } } return conversationViewIndex + 4 } lastMessageIndex := getLastMessageIndex() getConversationTopBar := func()(*fyne.Container, error){ theyAreBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(theirIdentityHash) if (err != nil){ return nil, err } if (theyAreBlocked == true){ blockedDescription := getBoldLabel("You Have Blocked This User.") blockedHelpButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ setViewPeerActionsPage(window, theirIdentityHash, currentPage) }) blockedDescriptionRow := container.NewHBox(layout.NewSpacer(), blockedDescription, blockedHelpButton, layout.NewSpacer()) return blockedDescriptionRow, nil } if (conversationExists == false) { topBar := getBoldLabelCentered("No messages exist.") return topBar, nil } getTopBar := func()*fyne.Container{ getMessagesViewingText := func()string{ if (numberOfMessages == 1 || lastMessageIndex == 0){ return "Viewing 1" } totalMessagesString := helpers.ConvertIntToString(numberOfMessages) if (numberOfMessages <= 5){ return "Viewing 1 - " + totalMessagesString } startNumberString := helpers.ConvertIntToString(conversationViewIndex + 1) lastMessageViewingString := helpers.ConvertIntToString(lastMessageIndex + 1) return "Viewing " + startNumberString + " - " + lastMessageViewingString } messagesViewingText := getMessagesViewingText() numberOfMessagesTotalString := helpers.ConvertIntToString(numberOfMessages) numberOfMessagesText := messagesViewingText + " of " + numberOfMessagesTotalString + " messages" if (numberOfMessages <= 5){ // No navigation buttons needed topBar := getBoldLabelCentered(numberOfMessagesText) return topBar } numberOfMessagesLabel := getBoldLabel(numberOfMessagesText) getNavigateToBeginningButton := func()fyne.Widget{ if (conversationViewIndex <= 0){ blankButton := widget.NewButton(" ", nil) return blankButton } viewBeginningButton := widget.NewButtonWithIcon("", theme.MediaFastRewindIcon(), func(){ err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, 0) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } currentPage() }) return viewBeginningButton } getViewOlderMessagesButton := func()fyne.Widget{ if (conversationViewIndex <= 0){ blankButton := widget.NewButton(" ", nil) return blankButton } viewOlderButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){ newViewIndex := conversationViewIndex - 5 err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, newViewIndex) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } currentPage() }) return viewOlderButton } getViewNewerMessagesButton := func()fyne.Widget{ if (conversationViewIndex >= maximumViewIndex){ blankButton := widget.NewButton(" ", nil) return blankButton } viewNewerButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){ newViewIndex := lastMessageIndex + 1 err := myConversationIndexes.SetConversationMessageViewIndex(myIdentityHash, theirIdentityHash, appNetworkType, newViewIndex) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } currentPage() }) return viewNewerButton } getNavigateToEndButton := func()fyne.Widget{ if (conversationViewIndex >= maximumViewIndex){ blankButton := widget.NewButton(" ", nil) return blankButton } navigateToEndButton := widget.NewButtonWithIcon("", theme.MediaFastForwardIcon(), currentPageWithNewestView) return navigateToEndButton } navigateToBeginningButton := getNavigateToBeginningButton() viewOlderMessagesButton := getViewOlderMessagesButton() viewNewerMessagesButton := getViewNewerMessagesButton() navigateToEndButton := getNavigateToEndButton() leftSideNavigationButtons := container.NewGridWithRows(1, navigateToBeginningButton, viewOlderMessagesButton) rightSideNavigationButton := container.NewGridWithRows(1, viewNewerMessagesButton, navigateToEndButton) topBar := container.NewHBox(layout.NewSpacer(), leftSideNavigationButtons, numberOfMessagesLabel, rightSideNavigationButton, layout.NewSpacer()) return topBar } topBar := getTopBar() if (iHaveRejectedThem == false && theyHaveRejectedMe == false){ return topBar, nil } getRejectionInfoDescription := func()string{ if (iHaveRejectedThem == true && theyHaveRejectedMe == false){ return "You have rejected this user." } if (iHaveRejectedThem == false && theyHaveRejectedMe == true){ return "This user has rejected you." } return "You have rejected eachother." } rejectionInfoDescription := getRejectionInfoDescription() rejectionInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ setGreetAndRejectExplainerPage(window, currentPage) }) rejectionInfoLabel := getBoldLabel(rejectionInfoDescription) rejectionInfoRow := container.NewHBox(layout.NewSpacer(), rejectionInfoLabel, rejectionInfoButton, layout.NewSpacer()) topBarWithDescription := container.NewVBox(topBar, widget.NewSeparator(), rejectionInfoRow) return topBarWithDescription, nil } getMessagesContainer := func()(*fyne.Container, error){ if (conversationExists == false){ emptyBox := getContainerBoxed(container.NewHBox()) return emptyBox, nil } // This will return true if we should pixelate received images // We will never pixelate images that we sent getPixelateReceivedImagesBool := func()(bool, error){ exists, pixelateStatus, err := mySettings.GetSetting("PixelateImagesOnOffStatus") if (err != nil) { return false, err } if (exists == false){ return true, nil } if (pixelateStatus == "Off"){ return false, nil } return true, nil } pixelateReceivedImagesBool, err := getPixelateReceivedImagesBool() if (err != nil){ return nil, err } if (lastMessageIndex > (len(nonSeenIndicatingMessagesList) - 1)){ return nil, errors.New("Maximum index out of range.") } messagesToDisplayList := nonSeenIndicatingMessagesList[conversationViewIndex:lastMessageIndex+1] messageRowsContainer := container.NewVBox() for _, messageObject := range messagesToDisplayList{ messageStatus := messageObject.MessageStatus messageCreationTime := messageObject.CreationTime messageCommunication := messageObject.Communication iAmSender := messageObject.IAmSender getMessageContentBox := func()(*fyne.Container, error){ isAPhoto := strings.HasPrefix(messageCommunication, ">!>Photo=") if (isAPhoto == true) { imageBase64 := strings.TrimPrefix(messageCommunication, ">!>Photo=") imageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(imageBase64) if (err != nil) { return nil, errors.New("MyChatMessages contains photo message with invalid photo: " + err.Error()) } getThumbnail := func()(*canvas.Image, error){ if (iAmSender == true || pixelateReceivedImagesBool == false){ fyneImageObject := canvas.NewImageFromImage(imageObject) return fyneImageObject, nil } photoIcon, err := getFyneImageIcon("Photo") if (err != nil) { return nil, err } return photoIcon, nil } imageThumbnail, err := getThumbnail() if (err != nil) { return nil, err } imageThumbnail.FillMode = canvas.ImageFillContain viewImageButton := getWidgetCentered(widget.NewButtonWithIcon("View Image", theme.VisibilityIcon(), func(){ if (iAmSender == true || pixelateReceivedImagesBool == false){ setViewFullpageImagePage(window, imageObject, currentPage) return } setSlowlyRevealImagePage(window, imageObject, 0, currentPage) })) imageWithButton := container.NewGridWithColumns(1, imageThumbnail, viewImageButton) imageWithButtonBoxed := getContainerBoxed(imageWithButton) return imageWithButtonBoxed, nil } isGreet := strings.HasPrefix(messageCommunication, ">!>Greet") if (isGreet == true){ greetIcon, err := getFyneImageIcon("Greet") if (err != nil) { return nil, err } emojiSize := getCustomFyneSize(20) greetIcon.SetMinSize(emojiSize) greetDescription := getBoldLabel("I Greet You.") greetHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setGreetAndRejectExplainerPage(window, currentPage) }) greetDescriptionRow := container.NewHBox(layout.NewSpacer(), greetDescription, greetHelpButton, layout.NewSpacer()) greetMessageBox := getContainerBoxed(container.NewVBox(greetIcon, greetDescriptionRow)) return greetMessageBox, nil } isReject := strings.HasPrefix(messageCommunication, ">!>Reject") if (isReject == true){ rejectIcon, err := getFyneImageIcon("Reject") if (err != nil) { return nil, err } emojiSize := getCustomFyneSize(20) rejectIcon.SetMinSize(emojiSize) rejectDescription := getBoldLabel("I Reject You.") rejectHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setGreetAndRejectExplainerPage(window, currentPage) }) rejectDescriptionRow := container.NewHBox(layout.NewSpacer(), rejectDescription, rejectHelpButton, layout.NewSpacer()) rejectMessageBox := getContainerBoxed(container.NewVBox(rejectIcon, rejectDescriptionRow)) return rejectMessageBox, nil } isEmoji := strings.HasPrefix(messageCommunication, ">!>Emoji=") if (isEmoji == true){ emojiIdentifierString := strings.TrimPrefix(messageCommunication, ">!>Emoji=") emojiIdentifierInt, err := helpers.ConvertStringToInt(emojiIdentifierString) if (err != nil){ return nil, errors.New("MyChatMessages contains invalid message: " + messageCommunication) } emojiImageObject, err := getEmojiImageObject(emojiIdentifierInt) if (err != nil) { return nil, err } emojiFyneImage := canvas.NewImageFromImage(emojiImageObject) emojiFyneImage.FillMode = canvas.ImageFillContain emojiSize := getCustomFyneSize(30) emojiFyneImage.SetMinSize(emojiSize) emojiImageBoxed := getFyneImageBoxed(emojiFyneImage) return emojiImageBoxed, nil } //TODO: Add Questionnaire // This will return true if we can display the unflattened and untrimmed message checkIfMessageTrimmingAndFlatteningIsNeeded := func()bool{ numberOfNewlines := strings.Count(messageCommunication, "\n") if (numberOfNewlines > 15){ // Message is too tall to show in full return true } // We need to count tabs as 5 runes numberOfTabs := strings.Count(messageCommunication, "\t") numberOfRunes := len([]rune(messageCommunication)) + (numberOfTabs*4) if (numberOfRunes > 200){ return true } return false } messageTrimmingAndFlatteningIsNeeded := checkIfMessageTrimmingAndFlatteningIsNeeded() if (messageTrimmingAndFlatteningIsNeeded == true){ messagePreviewText, _, err := helpers.TrimAndFlattenString(messageCommunication, 200) if (err != nil) { return nil, err } messagePreviewLabel := widget.NewLabel(messagePreviewText) messagePreviewLabel.TextStyle = getFyneTextStyle_Bold() messagePreviewLabel.Wrapping = 3 viewFullMessageButton := getWidgetCentered(widget.NewButtonWithIcon("Read All", theme.VisibilityIcon(), func(){ setViewTextPage(window, "Viewing Message", messageCommunication, false, currentPage) })) messageContentBoxed := getContainerBoxed(container.NewVBox(messagePreviewLabel, viewFullMessageButton)) return messageContentBoxed, nil } messageContentTextLabel := widget.NewLabel(messageCommunication) messageContentTextLabel.TextStyle = getFyneTextStyle_Bold() messageContentTextLabel.Wrapping = 3 messageContentTextBoxed := getWidgetBoxed(messageContentTextLabel) return messageContentTextBoxed, nil } // This will show a button to represent Queued, Failed, Seen, or Unseen getMessageStatusButton := func()(fyne.Widget, error){ if (messageStatus == "Queued"){ queuedButton := widget.NewButtonWithIcon("Queued", theme.HistoryIcon(), func(){ dialogTitle := translate("Message Is Queued") dialogMessageA := getLabelCentered(translate("This message is queued.")) dialogMessageB := getLabelCentered(translate("Seekia is still trying to send the message.")) dialogMessageC := getLabelCentered(translate("This could take a while if the recipient's chat keys are missing.")) dialogMessageD := getLabelCentered(translate("In that case, Seekia will try to download the recipient's chat keys.")) dialogMessageE := getLabelCentered(translate("If Seekia cannot find the user's chat keys, it will eventually stop trying.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC, dialogMessageD, dialogMessageE) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) }) queuedButton.Importance = widget.HighImportance return queuedButton, nil } if (messageStatus == "Failed"){ failedButton := widget.NewButtonWithIcon("Failed", theme.CancelIcon(), func(){ dialogTitle := translate("Message Failed") dialogMessageA := getLabelCentered(translate("This message failed to send.")) dialogMessageB := getLabelCentered(translate("This is probably due to the user's profile being missing or disabled.")) dialogMessageC := getLabelCentered(translate("You can try again.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) }) failedButton.Importance = widget.DangerImportance return failedButton, nil } // messageStatus == "Sent" messageHash := messageObject.MessageHash messageSeenUnseenStatus := getMessageIsSeenStatus(iAmSender, messageHash) if (messageSeenUnseenStatus == false && iAmSender == true){ unseenButton := widget.NewButtonWithIcon("Unseen", theme.VisibilityOffIcon(), func(){ title := translate("Message is Unseen") dialogMessageA := getLabelCentered(translate("This message is unseen.")) dialogMessageB := getLabelCentered(translate("The recipient has not indicated that they have seen the message.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(title, translate("Close"), dialogContent, window) }) return unseenButton, nil } if (messageSeenUnseenStatus == false && iAmSender == false){ unseenButton := widget.NewButtonWithIcon("Unseen", theme.VisibilityOffIcon(), func(){ setConfirmSendSeenMessagePage(window, myIdentityHash, theirIdentityHash, messageHash, currentPage, currentPage) }) return unseenButton, nil } if (messageSeenUnseenStatus == true && iAmSender == true){ seenButton := widget.NewButtonWithIcon("Seen", theme.VisibilityIcon(), func(){ title := translate("Message Is Seen") dialogMessage := getLabelCentered(translate("This message has been seen by the recipient.")) dialogContent := container.NewVBox(dialogMessage) dialog.ShowCustom(title, translate("Close"), dialogContent, window) }) seenButton.Importance = widget.HighImportance return seenButton, nil } // messageSeenUnseenStatus == true && iAmSender == false seenButton := widget.NewButtonWithIcon("Seen", theme.VisibilityIcon(), func(){ title := translate("Message Is Seen") dialogMessage := getLabelCentered(translate("You have notified the recipient that you have seen this message.")) dialogContent := container.NewVBox(dialogMessage) dialog.ShowCustom(title, translate("Close"), dialogContent, window) }) seenButton.Importance = widget.HighImportance return seenButton, nil } messageContentBox, err := getMessageContentBox() if (err != nil) { return nil, err } timeSentAgoText, err := helpers.ConvertUnixTimeToTimeFromNowTranslated(messageCreationTime, false) if (err != nil) { return nil, err } timeSentAgoLabel := getItalicLabel("Sent " + timeSentAgoText) getViewMessageDetailsButton := func()(fyne.Widget, error){ if (messageStatus == "Sent"){ messageHash := messageObject.MessageHash viewMessageDetailsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ setViewMessageDetailsButton(window, messageHash, messageCreationTime, currentPage) }) return viewMessageDetailsButton, nil } if (messageStatus == "Failed"){ showFailedDialog := func(){ //TODO: Add a page where user can see why message failed dialogTitle := translate("Message Failed") dialogMessageA := getLabelCentered(translate("This message failed to send.")) dialogMessageB := getLabelCentered(translate("This is probably due to the user's profile being missing or disabled.")) dialogMessageC := getLabelCentered(translate("You can try again.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) } viewMessageDetailsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), showFailedDialog) return viewMessageDetailsButton, nil } if (messageStatus == "Queued"){ showQueuedDialog := func(){ //TODO: Add a page where user can cancel message, and see why message is queued dialogTitle := translate("Message Is Queued") dialogMessageA := getLabelCentered(translate("This message is queued.")) dialogMessageB := getLabelCentered(translate("Seekia is still trying to send the message.")) dialogMessageC := getLabelCentered(translate("This could take a while if the recipient's chat keys are missing.")) dialogMessageD := getLabelCentered(translate("In that case, Seekia will try to download the recipient's chat keys.")) dialogMessageE := getLabelCentered(translate("If Seekia cannot find the user's chat keys, it will eventually stop trying.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC, dialogMessageD, dialogMessageE) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) } viewMessageDetailsButton := widget.NewButtonWithIcon("", theme.InfoIcon(), showQueuedDialog) return viewMessageDetailsButton, nil } return nil, errors.New("messagesToDisplayList contains messageMap with invalid messageStatus: " + messageStatus) } viewMessageDetailsButton, err := getViewMessageDetailsButton() if (err != nil) { return nil, err } messageDetailsRow := container.NewHBox(layout.NewSpacer(), timeSentAgoLabel, viewMessageDetailsButton, layout.NewSpacer()) messageStatusButton, err := getMessageStatusButton() if (err != nil) { return nil, err } //messageDetailsBox := getContainerBoxed(container.NewVBox(messageStatusButton, messageDetailsRow)) getMessageRow := func()*fyne.Container{ if (iAmSender == true){ messageBox := getContainerBoxed(container.NewBorder(nil, messageDetailsRow, messageStatusButton, nil, messageContentBox)) emptyLabel := widget.NewLabel("") messageRow := container.NewBorder(nil, nil, emptyLabel, nil, messageBox) return messageRow } messageBox := getContainerBoxed(container.NewBorder(nil, messageDetailsRow, nil, messageStatusButton, messageContentBox)) emptyLabel := widget.NewLabel("") messageRow := container.NewBorder(nil, nil, nil, emptyLabel, messageBox) return messageRow } messageRow := getMessageRow() messageRowsContainer.Add(messageRow) } if (conversationViewIndex >= maximumViewIndex){ refreshMessagesButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh", theme.ViewRefreshIcon(), currentPageWithNewestView)) messageRowsContainer.Add(refreshMessagesButton) } messageRowsScrollable := container.NewVScroll(messageRowsContainer) boxedMessagesContainer := getScrollContainerBoxed(messageRowsScrollable) return boxedMessagesContainer, nil } getChooseMessageTypeToSendRow := func()(*fyne.Container, error){ // Outputs: // -bool: Able to send showDialogIfUnableToSend := func()bool{ if (iAmDisabled == true){ dialogTitle := translate("Unable To Chat") dialogMessageA := getLabelCentered(translate("Cannot respond: Your identity is disabled.")) dialogMessageB := getLabelCentered(translate("Enable your profile on the Profiles - Broadcast page.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return false } if (theyAreDisabled == true){ dialogTitle := translate("Unable To Chat") dialogMessageA := getLabelCentered(translate("Cannot respond: Their identity is disabled.")) dialogMessageB := getLabelCentered(translate("This user has disabled their Seekia profile.")) //TODO: Add button to attempt to download their profile dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return false } return true } sendImageButton := widget.NewButtonWithIcon("Send Image", theme.FileImageIcon(), func(){ ableToSend := showDialogIfUnableToSend() if (ableToSend == false){ return } setSendChatImagePage(window, myIdentityHash, theirIdentityHash, currentPage, currentPageWithNewestView) }) sendTextButton := widget.NewButtonWithIcon("Send Text", theme.DocumentCreateIcon(), func(){ ableToSend := showDialogIfUnableToSend() if (ableToSend == false){ return } setWriteMessageToSendPage(window, myIdentityHash, theirIdentityHash, "", currentPage, currentPageWithNewestView) }) sendEmojiButton := widget.NewButtonWithIcon("Send Emoji", theme.AccountIcon(), func(){ ableToSend := showDialogIfUnableToSend() if (ableToSend == false){ return } submitEmojiFunction := func(emojiIdentifier int){ emojiIdentifierString := helpers.ConvertIntToString(emojiIdentifier) newMessageCommunication := ">!>Emoji=" + emojiIdentifierString setFundAndSendChatMessagePage(window, myIdentityHash, theirIdentityHash, newMessageCommunication, false, currentPage, currentPageWithNewestView) } setChooseEmojiPage(window, "Choose Emoji", "Circle Face", 0, currentPage, submitEmojiFunction) }) sendMessageRow := getContainerCentered(container.NewGridWithRows(1, sendImageButton, sendTextButton, sendEmojiButton)) return sendMessageRow, nil } topbar, err := getConversationTopBar() if (err != nil) { return nil, err } messagesContainer, err := getMessagesContainer() if (err != nil){ return nil, err } chooseMessageTypeToSendRow, err := getChooseMessageTypeToSendRow() if (err != nil) { return nil, err } conversationMessagesColumn := container.NewBorder(topbar, chooseMessageTypeToSendRow, nil, nil, messagesContainer) err = myReadStatus.SetConversationReadUnreadStatus(myIdentityHash, theirIdentityHash, appNetworkType, "Read") if (err != nil) { return nil, err } return conversationMessagesColumn, nil } conversationContainer, err := getConversationContainer() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } pageHeader := container.NewVBox(title, backButton, widget.NewSeparator()) pageContent := container.NewBorder(pageHeader, nil, recipientInfoColumn, myInfoColumn, conversationContainer) setPageContent(pageContent, window) } func setViewMessageDetailsButton(window fyne.Window, messageHash [26]byte, messageCreationTime int64, previousPage func()){ currentPage := func(){setViewMessageDetailsButton(window, messageHash, messageCreationTime, previousPage)} title := getPageTitleCentered("View Message Details") backButton := getBackButtonCentered(previousPage) subtitle := getPageSubtitleCentered("Message Details") messageHashLabel := widget.NewLabel("Message Hash:") messageHashHex := encoding.EncodeBytesToHexString(messageHash[:]) messageHashTrimmed, _, err := helpers.TrimAndFlattenString(messageHashHex, 10) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } messageHashText := getBoldLabel(messageHashTrimmed) viewMessageHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewContentHashPage(window, "Message", messageHash[:], currentPage) }) messageHashRow := container.NewHBox(layout.NewSpacer(), messageHashLabel, messageHashText, viewMessageHashButton, layout.NewSpacer()) timeCreatedLabel := widget.NewLabel("Time Created:") messageCreationTimeString := helpers.ConvertUnixTimeToTranslatedTime(messageCreationTime) messageCreationTimeLabel := getBoldLabel(messageCreationTimeString) creationTimeWarningButton := widget.NewButtonWithIcon("", theme.WarningIcon(), func(){ title := translate("Creation Time Warning") dialogMessageA := getLabelCentered("Creation times are not verified.") dialogMessageB := getLabelCentered("They can be faked by the message author.") dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(title, translate("Close"), dialogContent, window) }) messageCreationTimeRow := container.NewHBox(layout.NewSpacer(), timeCreatedLabel, messageCreationTimeLabel, creationTimeWarningButton, layout.NewSpacer()) reportMessageButton := getWidgetCentered(widget.NewButtonWithIcon("Report Message", theme.WarningIcon(), func(){ //TODO // This will go to a page where the user can report the message // They will have to pay for their report, and be warned that the report may contain their identity hash // It will not be directly linked to their identity hash if the message was sent to a secret inbox. // We should let the user know if the message was sent to a secret inbox or not. // The sender will also be able to tell that their message was reported // If the message contains illegal content, reporting the message may be legally risky for the recipient, because it acknowledges // that the user saw the message // We must warn the user of this too. showUnderConstructionDialog(window) })) //TODO: Show other information: Message size, message moderation status, and when message will expire from network page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), messageHashRow, widget.NewSeparator(), messageCreationTimeRow, widget.NewSeparator(), reportMessageButton) setPageContent(page, window) } func setConfirmSendSeenMessagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, seenMessageHash [26]byte, previousPage func(), afterMessageSentPage func()){ currentPage := func(){setConfirmSendSeenMessagePage(window, myIdentityHash, recipientIdentityHash, seenMessageHash, previousPage, afterMessageSentPage)} title := getPageTitleCentered(translate("Send Seen Message")) backButton := getBackButtonCentered(previousPage) subtitle := getPageSubtitleCentered("Confirm Send Seen Message?") description1 := getLabelCentered("Are you sure you want to send a seen message?") description2 := getLabelCentered("This will let the user know you received and saw their message.") description3 := getLabelCentered("This is informative for the recipient.") description4 := getLabelCentered("You will pay for the message on the next page.") confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){ seenMessageHashHex := encoding.EncodeBytesToHexString(seenMessageHash[:]) seenMessageCommunication := ">!>Seen=" + seenMessageHashHex setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, seenMessageCommunication, false, currentPage, afterMessageSentPage) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, confirmButton) setPageContent(page, window) } func setWriteMessageToSendPage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageCommunication string, previousPage func(), afterMessageSentPage func()){ title := getPageTitleCentered("Write Message") backButton := getBackButtonCentered(previousPage) messageEntry := widget.NewMultiLineEntry() messageEntry.Wrapping = 3 messageEntry.SetPlaceHolder(translate("Enter message...")) messageEntry.Text = messageCommunication // We use this function so user does not lose their written message if they view Rules/SendMessage page and return back currentPageWithEntryContent := func(){ currentEntryText := messageEntry.Text setWriteMessageToSendPage(window, myIdentityHash, recipientIdentityHash, currentEntryText, previousPage, afterMessageSentPage) } description1 := getBoldLabelCentered("Write your message to send.") description2 := widget.NewLabel("Your message must follow the Seekia rules.") seekiaRulesButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setViewSeekiaRulesPage(window, currentPageWithEntryContent) }) description2Row := container.NewHBox(layout.NewSpacer(), description2, seekiaRulesButton, layout.NewSpacer()) sendButton := widget.NewButtonWithIcon("Send", theme.NavigateNextIcon(), func(){ //TODO: Length limit newMessageCommunication := messageEntry.Text if (newMessageCommunication == ""){ return } textIsAllowed := allowedText.VerifyStringIsAllowed(newMessageCommunication) if (textIsAllowed == false){ dialogTitle := translate("Message Is Not Allowed") dialogMessageA := getLabelCentered("Text contains unallowed text.") dialogMessageB := getLabelCentered("It must be encoded in UTF-8.") dialogMessageC := getLabelCentered("Remove unallowed text and resend message.") dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } // We use this prefix for special messages (emojis, questionnaire responses, images, seen messages) isSpecial := strings.HasPrefix(newMessageCommunication, ">!>") if (isSpecial == true){ dialogTitle := translate("Message Prefix Is Not Allowed") dialogMessageA := getLabelCentered("Your message cannot start with >!>.") dialogMessageB := getLabelCentered("Remove this prefix and resend message.") dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window) return } setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, newMessageCommunication, false, currentPageWithEntryContent, afterMessageSentPage) }) sendButtonCentered := getWidgetCentered(sendButton) spacer := widget.NewLabel("") sendButtonWithSpacer := container.NewVBox(sendButtonCentered, spacer) header := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2Row, widget.NewSeparator()) page := container.NewBorder(header, sendButtonWithSpacer, nil, nil, messageEntry) setPageContent(page, window) } func setSendChatImagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, previousPage func(), afterSendPage func()){ currentPage := func(){setSendChatImagePage(window, myIdentityHash, recipientIdentityHash, previousPage, afterSendPage)} title := getPageTitleCentered("Send Chat Image") backButton := getBackButtonCentered(previousPage) subtitle := getPageSubtitleCentered("Send Image") description1 := getBoldLabelCentered("Select an image file to send.") description2 := getLabelCentered("You can apply image effects on the next page.") description3 := getLabelCentered("JPEG, PNG and WEBP files are supported.") openFileCallbackFunction := func(fileObject fyne.URIReadCloser, err error){ if (err != nil) { title := translate("Failed to open image file.") dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: " + err.Error())) dialogContent := container.NewVBox(dialogMessage) dialog.ShowCustom(title, translate("Close"), dialogContent, window) return } if (fileObject == nil) { return } setLoadingScreen(window, "Add Image", "Importing image...") filePath := fileObject.URI().String() filePath = strings.TrimPrefix(filePath, "file://") fileExists, ableToReadImage, imageObject, err := imagery.ReadImageFile(filePath) if (err != nil) { currentPage() title := translate("Failed to open image file.") dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: " + err.Error())) dialogContent := container.NewVBox(dialogMessage) dialog.ShowCustom(title, translate("Close"), dialogContent, window) return } if (fileExists == false) { currentPage() title := translate("Failed to open image file.") dialogMessage := getLabelCentered(translate("Report this error to Seekia developers: Image file not found.")) dialogContent := container.NewVBox(dialogMessage) dialog.ShowCustom(title, translate("Close"), dialogContent, window) return } if (ableToReadImage == false) { currentPage() title := translate("Failed to import image file.") dialogMessageA := getLabelCentered(translate("Seekia only supports these image file formats:")) dialogMessageB := getLabelCentered("JPG, JPEG, PNG, WEBP") dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(title, translate("Close"), dialogContent, window) return } // We resize image to something that will be more managable when we edit it // When we export it, we will downsize it even further imageObjectResized, err := imagery.DownsizeGolangImage(imageObject, 1500) if (err != nil) { currentPage() title := translate("Failed To Process Image File") dialogMessageA := getLabelCentered(translate("Your file may be too large.")) errorString := err.Error() errorTrimmed, _, err := helpers.TrimAndFlattenString(errorString, 20) if (err != nil){ setErrorEncounteredPage(window, err, currentPage) return } errorTrimmedLabel := getBoldLabel("Error: " + errorTrimmed) viewFullErrorButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewTextPage(window, "Viewing Error", errorString, false, currentPage) }) errorDescriptionRow := container.NewHBox(layout.NewSpacer(), errorTrimmedLabel, viewFullErrorButton, layout.NewSpacer()) dialogContent := container.NewVBox(dialogMessageA, errorDescriptionRow) dialog.ShowCustom(title, translate("Close"), dialogContent, window) return } submitImageToSendFunction := func(newImageObject image.Image, newPreviousPage func()){ setLoadingScreen(window, "Send Chat Image", "Compressing Image...") newImageBase64String, err := imagery.ConvertImageObjectToStandardWebpBase64String(newImageObject) if (err != nil) { setErrorEncounteredPage(window, err, newPreviousPage) return } setConfirmSendChatImagePage(window, myIdentityHash, recipientIdentityHash, newImageBase64String, newPreviousPage, afterSendPage) } setEditImagePage(window, imageObject, false, nil, imageObjectResized, currentPage, submitImageToSendFunction) } selectImageFileButton := getWidgetCentered(widget.NewButtonWithIcon("Select Image File", theme.FileImageIcon(), func(){ dialog.ShowFileOpen(openFileCallbackFunction, window) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, selectImageFileButton) setPageContent(page, window) } func setConfirmSendChatImagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, newImageBase64String string, previousPage func(), afterSendPage func()){ currentPage := func(){setConfirmSendChatImagePage(window, myIdentityHash, recipientIdentityHash, newImageBase64String, previousPage, afterSendPage)} title := getPageTitleCentered("Send Chat Image") backButton := getBackButtonCentered(previousPage) subtitle := getPageSubtitleCentered("Confirm Send Image?") description1 := getLabelCentered("Are you sure you want to send this image?") description2 := getLabelCentered("Be aware that the image has been compressed.") description3 := getLabelCentered("You will pay for the message on the next page.") submitButton := getWidgetCentered(widget.NewButtonWithIcon("Send Image", theme.ConfirmIcon(), func(){ newMessageCommunication := ">!>Photo=" + newImageBase64String setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, newMessageCommunication, false, currentPage, afterSendPage) })) croppedImageObject, err := imagery.ConvertWEBPBase64StringToCroppedDownsizedImageObject(newImageBase64String) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } newFyneImage := canvas.NewImageFromImage(croppedImageObject) newFyneImage.FillMode = canvas.ImageFillContain viewFullpageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){ setViewFullpageImagePage(window, croppedImageObject, currentPage) })) emptyLabel := widget.NewLabel("") zoomButtonWithSpacer := container.NewVBox(viewFullpageButton, emptyLabel) header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), submitButton, widget.NewSeparator()) page := container.NewBorder(header, zoomButtonWithSpacer, nil, nil, newFyneImage) setPageContent(page, window) } // Network duration time is the time the message should exist on the network before expiring func setFundAndSendChatMessagePage(window fyne.Window, myIdentityHash [16]byte, recipientIdentityHash [16]byte, messageCommunication string, acceptedMessageQueueWarning bool, previousPage func(), afterMessageSentPage func()){ setLoadingScreen(window, "Send Message", "Loading...") currentPage := func(){setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, messageCommunication, acceptedMessageQueueWarning, previousPage, afterMessageSentPage)} isMine, myIdentityType, err := myIdentity.CheckIfIdentityHashIsMine(myIdentityHash) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (isMine == false){ // should not occur, as send chat message page will restrict user from sending message if their identity does not exist setErrorEncounteredPage(window, errors.New("Your identity no longer exists."), previousPage) return } if (myIdentityType != "Mate" && myIdentityType != "Moderator"){ // This should never occur setErrorEncounteredPage(window, errors.New("Attempting to send from invalid identity type: " + myIdentityType), previousPage) return } title := getPageTitleCentered("Send Message") backButton := getBackButtonCentered(previousPage) exists, iAmDisabled, err := myLocalProfiles.GetProfileData(myIdentityType, "Disabled") if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (exists == true && iAmDisabled == "Yes"){ description1 := getBoldLabelCentered("Your profile is disabled.") description2 := getLabelCentered("You must enable your profile to send messages.") description3 := getLabelCentered("Enable your profile on the Profile - Broadcast page.") page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3) setPageContent(page, window) return } appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } myIdentityFound, myProfileIsActiveStatus, err := myProfileStatus.GetMyProfileIsActiveStatus(myIdentityHash, appNetworkType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (myIdentityFound == false) { setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), previousPage) return } if (myProfileIsActiveStatus == false){ description1 := getBoldLabelCentered("Your profile is not active.") description2 := getLabelCentered("You must broadcast your profile to chat with users.") description3 := getLabelCentered("Broadcast your " + myIdentityType + " profile on the Broadcast page.") broadcastPageButton := getWidgetCentered(widget.NewButton("Visit Broadcast Page", func(){ setBroadcastPage(window, myIdentityType, currentPage) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, broadcastPageButton) setPageContent(page, window) return } recipientIdentityType, err := identity.GetIdentityTypeFromIdentityHash(recipientIdentityHash) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } if (recipientIdentityType == "Host"){ description1 := getBoldLabelCentered("Recipient is a Host.") description2 := getLabelCentered("They cannot be chatted with.") page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2) setPageContent(page, window) return } // We check if the recipient is disabled getRecipientIsDisabledBool := func()(bool, error){ recipientProfileExists, _, _, _, _, recipientRawProfileMap, err := profileStorage.GetNewestUserProfile(recipientIdentityHash, appNetworkType) if (err != nil) { return false, err } if (recipientProfileExists == false){ return false, nil } recipientIsDisabled, _, err := readProfiles.GetFormattedProfileAttributeFromRawProfileMap(recipientRawProfileMap, "Disabled") if (err != nil) { return false, err } return recipientIsDisabled, nil } recipientIsDisabled, err := getRecipientIsDisabledBool() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } showPeerIsDisabledPage := func(){ userIsDisabledDescriptionA := getBoldLabelCentered("The recipient has disabled their profile.") userIsDisabledDescriptionB := getLabelCentered("You cannot send them a message.") page := container.NewVBox(title, backButton, widget.NewSeparator(), userIsDisabledDescriptionA, userIsDisabledDescriptionB) setPageContent(page, window) } if (recipientIsDisabled == true){ showPeerIsDisabledPage() return } peerIsDisabled, peerChatKeysExist, _, _, err := peerChatKeys.GetPeerNewestActiveChatKeys(recipientIdentityHash, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (peerIsDisabled == true){ showPeerIsDisabledPage() return } if (peerChatKeysExist == false && acceptedMessageQueueWarning == false){ description1 := getBoldLabelCentered("You do not have the recipient's chat keys.") description2 := getLabelCentered("You can add your message to the message queue.") description3 := getLabelCentered("Seekia will try to download their chat keys in the background.") description4 := getLabelCentered("You can also try to download their profile immediately.") description5 := getLabelCentered("Add message to message queue, or try to download chat keys?") addToMessageQueueButton := getWidgetCentered(widget.NewButtonWithIcon("Add Message To Message Queue", theme.NavigateNextIcon(), func(){ setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, messageCommunication, true, previousPage, afterMessageSentPage) })) downloadProfileButton := getWidgetCentered(widget.NewButtonWithIcon("Download Profile", theme.DownloadIcon(), func(){ setDownloadMissingUserProfilePage(window, recipientIdentityHash, true, false, previousPage, currentPage, previousPage) })) buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, addToMessageQueueButton, downloadProfileButton)) page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, buttonsGrid) setPageContent(page, window) return } //Outputs: // -bool: Parameters Exist // -*fyne.Container: // -error getSendMessageContent := func()(bool, *fyne.Container, error){ parametersExist, err := getParameters.CheckIfChatParametersExist(appNetworkType) if (err != nil) { return false, nil, err } if (parametersExist == false){ return false, nil, nil } currentCreditBalanceLabel := getLabelCentered("My Credit Balance:") 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){ return false, nil, err } parametersExist, appCurrencyAccountCreditBalance, err := myAccountCredit.GetMyCreditAccountBalanceInAnyCurrency(myIdentityType, appNetworkType, currentAppCurrencyCode) if (err != nil) { return false, nil, err } if (parametersExist == false){ return false, nil, nil } _, appCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentAppCurrencyCode) if (err != nil) { return false, nil, err } appCurrencyCreditBalanceString := helpers.ConvertFloat64ToStringRounded(appCurrencyAccountCreditBalance, 3) appCurrencySymbolButton := widget.NewButton(appCurrencySymbol, func(){ setChangeAppCurrencyPage(window, currentPage) }) currentBalanceLabel := getBoldLabel(appCurrencyCreditBalanceString + " " + currentAppCurrencyCode) currentBalanceRow := container.NewHBox(layout.NewSpacer(), appCurrencySymbolButton, currentBalanceLabel, layout.NewSpacer()) manageAccountCreditButton := getWidgetCentered(widget.NewButton("Manage", func(){ setViewMyAccountCreditPage(window, myIdentityType, currentPage) })) estimatedMessageSize, err := sendMessages.GetEstimatedMessageSize(recipientIdentityHash, messageCommunication) if (err != nil) { return false, nil, err } messageCostLabel := getBoldLabelCentered("Message Cost:") appCurrencyCostBinding := binding.NewString() durationDaysBinding := binding.NewInt() appCurrencyCostLabel := widget.NewLabelWithData(appCurrencyCostBinding) appCurrencyCostLabel.TextStyle = getFyneTextStyle_Bold() appCurrencyCostLabelCentered := container.NewHBox(layout.NewSpacer(), appCurrencyCostLabel, layout.NewSpacer()) // Fewer options improves anonymity, as a user who selects the same option every time will stand out //Outputs: // -bool: Parameters exist // -error networkExpirationSelectFunction := func(response string)(bool, error){ getDurationDays := func()int{ if (response == "2 Days"){ return 2 } // response == "2 Weeks" return 14 } desiredDurationDays := getDurationDays() desiredDurationSeconds := desiredDurationDays * 86400 parametersExist, appCurrencyCost, err := sendMessages.GetMessageNetworkCurrencyCostForProvidedDuration(appNetworkType, estimatedMessageSize, desiredDurationSeconds, currentAppCurrencyCode) if (err != nil) { return false, err } if (parametersExist == false){ return false, nil } currencyCostString := helpers.ConvertFloat64ToStringRounded(appCurrencyCost, 3) err = durationDaysBinding.Set(desiredDurationDays) if (err != nil) { return false, err } err = appCurrencyCostBinding.Set(appCurrencySymbol + currencyCostString + " " + currentAppCurrencyCode) if (err != nil) { return false, err } return true, nil } // We initialize the bindings parametersExist, err = networkExpirationSelectFunction("2 Days") if (err != nil) { return false, nil, err } if (parametersExist == false){ return false, nil, nil } durationToFundText := getBoldLabelCentered("Duration To fund:") expirationOptionsList := []string{"2 Days", "2 Weeks"} networkDurationSelector := widget.NewSelect(expirationOptionsList, func(response string){ parametersExist, err := networkExpirationSelectFunction(response) if (err != nil){ setErrorEncounteredPage(window, err, currentPage) } if (parametersExist == false){ // Parameter permissions must have been updated and now existing parameters are invalid // We will refresh the page // We will show a loading screen so if this causes an infinite loop, we can detect it setLoadingScreen(window, "Send Chat Message", "Trying to find parameters...") time.Sleep(time.Second) currentPage() } }) networkDurationSelector.Selected = "2 Days" networkDurationSelectorCentered := getWidgetCentered(networkDurationSelector) payAndSendButton := getWidgetCentered(widget.NewButtonWithIcon("Pay and Send", theme.ConfirmIcon(), func(){ // Outputs: // -bool: Parameters exist // -bool: Sufficient credit exist // -error sendMessageFunction := func()(bool, bool, error){ messageDaysToFund, err := durationDaysBinding.Get() if (err != nil){ return false, false, err } messageDurationToFund := messageDaysToFund * 86400 parametersExist, sufficientCreditExist, err := myMessageQueue.AddMessageToMyMessageQueue(myIdentityHash, recipientIdentityHash, appNetworkType, messageDurationToFund, messageCommunication) if (err != nil) { return false, false, err } if (parametersExist == false){ return false, false, nil } if (sufficientCreditExist == false){ return true, false, nil } //TODO: Check to see if their identity balance is known to be expired, here and within the send message code return true, true, nil } parametersExist, sufficientCreditExist, err := sendMessageFunction() if (err != nil){ setErrorEncounteredPage(window, err, currentPage) return } if (parametersExist == false){ // Parameter permissions must have been updated and now existing parameters are invalid // We will refresh the page // We will show a loading screen so if this causes an infinite loop, we can detect it setLoadingScreen(window, "Send Chat Message", "Trying to find parameters.") time.Sleep(time.Second) currentPage() return } if (sufficientCreditExist == false){ title := translate("Insufficient Credit") dialogMessageA := getLabelCentered(translate("You do not have enough credit to send this message.")) dialogMessageB := getLabelCentered(translate("Add more credit on the Add Credit page.")) dialogContent := container.NewVBox(dialogMessageA, dialogMessageB) dialog.ShowCustom(title, translate("Close"), dialogContent, window) return } afterMessageSentPage() })) sendMessageContent := container.NewVBox(currentCreditBalanceLabel, currentBalanceRow, manageAccountCreditButton, widget.NewSeparator(), messageCostLabel, appCurrencyCostLabelCentered, widget.NewSeparator(), durationToFundText, networkDurationSelectorCentered, payAndSendButton) return true, sendMessageContent, nil } parametersExist, sendMessageContent, err := getSendMessageContent() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (parametersExist == false){ //TODO: Add button to view download progress and check network connection missingParametersLabelA := getBoldLabelCentered("Network parameters are not downloaded.") missingParametersLabelB := getLabelCentered("Please wait for them to download.") page := container.NewVBox(title, backButton, widget.NewSeparator(), missingParametersLabelA, missingParametersLabelB) setPageContent(page, window) return } page := container.NewVBox(title, backButton, widget.NewSeparator(), sendMessageContent) setPageContent(page, window) } func setChatStatisticsPage(window fyne.Window, identityType string, previousPage func()){ pageIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) checkIfPageHasChangedFunction := func()bool{ exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") if (exists == true && currentViewedPage == pageIdentifier){ return false } return true } currentPage := func(){setChatStatisticsPage(window, identityType, previousPage)} title := getPageTitleCentered(identityType + " Chat Statistics") backButton := getBackButtonCentered(previousPage) subtitle := getPageSubtitleCentered("My " + identityType + " Chat Statistics") numberOfInboxMessagesBinding := binding.NewString() numberOfConversationsBinding := binding.NewString() numberOfUnreadConversationsBinding := binding.NewString() numberOfInboxMessagesTitle := widget.NewLabel("Number Of Inbox Messages:") numberOfInboxMessagesLabel := widget.NewLabelWithData(numberOfInboxMessagesBinding) numberOfInboxMessagesLabel.TextStyle = getFyneTextStyle_Bold() numberOfInboxMessagesRow := container.NewHBox(layout.NewSpacer(), numberOfInboxMessagesTitle, numberOfInboxMessagesLabel, layout.NewSpacer()) numberOfConversationsTitle := widget.NewLabel("Number Of Conversations:") numberOfConversationsLabel := widget.NewLabelWithData(numberOfConversationsBinding) numberOfConversationsLabel.TextStyle = getFyneTextStyle_Bold() numberOfConversationsRow := container.NewHBox(layout.NewSpacer(), numberOfConversationsTitle, numberOfConversationsLabel, layout.NewSpacer()) numberOfUnreadConversationsTitle := widget.NewLabel("Number Of Unread Conversations:") numberOfUnreadConversationsLabel := widget.NewLabelWithData(numberOfUnreadConversationsBinding) numberOfUnreadConversationsLabel.TextStyle = getFyneTextStyle_Bold() numberOfUnreadConversationsRow := container.NewHBox(layout.NewSpacer(), numberOfUnreadConversationsTitle, numberOfUnreadConversationsLabel, layout.NewSpacer()) appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } updateBindingsFunction := func(){ err := myChatConversations.StartUpdatingMyConversations(identityType, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } var resultsReadyBoolMutex sync.RWMutex resultsReadyBool := false updateLoadingProgress := func(){ secondsElapsed := 0 for { resultsReadyBoolMutex.RLock() resultsReady := resultsReadyBool resultsReadyBoolMutex.RUnlock() if (resultsReady == true){ return } pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } if (secondsElapsed%3 == 0){ numberOfInboxMessagesBinding.Set("Loading.") numberOfConversationsBinding.Set("Loading.") numberOfUnreadConversationsBinding.Set("Loading.") } else if (secondsElapsed %3 == 1){ numberOfInboxMessagesBinding.Set("Loading..") numberOfConversationsBinding.Set("Loading..") numberOfUnreadConversationsBinding.Set("Loading..") } else { numberOfInboxMessagesBinding.Set("Loading...") numberOfConversationsBinding.Set("Loading...") numberOfUnreadConversationsBinding.Set("Loading...") } time.Sleep(time.Second) secondsElapsed += 1 } } go updateLoadingProgress() numberOfInboxMessages, err := myChatMessages.GetNumberOfMessagesInMyInbox(identityType, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } numberOfInboxMessagesString := helpers.ConvertIntToString(numberOfInboxMessages) numberOfInboxMessagesBinding.Set(numberOfInboxMessagesString) for { pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } conversationsReady, numberOfConversations, err := myChatConversations.GetNumberOfConversations(identityType, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (conversationsReady == false){ continue } numberOfConversationsString := helpers.ConvertIntToString(numberOfConversations) numberOfConversationsBinding.Set(numberOfConversationsString) conversationsReady, numberOfUnreadConversations, err := myChatConversations.GetNumberOfUnreadConversations(identityType, appNetworkType) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } if (conversationsReady == false){ continue } numberOfUnreadConversationsString := helpers.ConvertIntToString(numberOfUnreadConversations) numberOfUnreadConversationsBinding.Set(numberOfUnreadConversationsString) resultsReadyBoolMutex.Lock() resultsReadyBool = true resultsReadyBoolMutex.Unlock() return } } viewChatFilterStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View My Chat Filter Statistics", theme.VisibilityIcon(), func(){ setViewMyChatFilterStatisticsPage(window, identityType, false, 0, 0, nil, currentPage) })) page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), numberOfInboxMessagesRow, numberOfConversationsRow, numberOfUnreadConversationsRow, widget.NewSeparator(), viewChatFilterStatisticsButton) setPageContent(page, window) go updateBindingsFunction() } func setViewMyChatFilterStatisticsPage(window fyne.Window, myIdentityType string, statisticsReady bool, numberOfConversations int, numberOfFilteredConversations int, statisticsItemsList []myChatFilterStatistics.ChatFilterStatisticsItem, previousPage func()){ pageIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier) checkIfPageHasChangedFunction := func()bool{ exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage") if (exists == true && currentViewedPage == pageIdentifier){ return false } return true } title := getPageTitleCentered("My " + myIdentityType + " Chat Filter Statistics") backButton := getBackButtonCentered(previousPage) if (statisticsReady == false){ appNetworkType, err := getAppNetworkType.GetAppNetworkType() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } loadingTextBinding := binding.NewString() loadingTextBinding.Set("Loading...") loadingTextLabel := widget.NewLabelWithData(loadingTextBinding) loadingTextLabel.TextStyle = getFyneTextStyle_Bold() loadingTextLabelCentered := getWidgetCentered(loadingTextLabel) calculateStatisticsAndRefreshPageFunction := func(){ var statisticsCompleteBoolMutex sync.RWMutex statisticsCompleteBool := false updateLoadingBindingFunction := func(){ secondsElapsed := 0 for { if (secondsElapsed%3 == 0){ loadingTextBinding.Set("Loading.") } else if (secondsElapsed%3 == 1){ loadingTextBinding.Set("Loading..") } else { loadingTextBinding.Set("Loading...") } pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == true){ return } statisticsCompleteBoolMutex.RLock() statisticsComplete := statisticsCompleteBool statisticsCompleteBoolMutex.RUnlock() if (statisticsComplete == true){ return } time.Sleep(time.Second) secondsElapsed += 1 } } go updateLoadingBindingFunction() numberOfConversations, numberOfFilteredConversations, myChatFilterStatisticsItemsList, err := myChatFilterStatistics.GetAllMyChatFilterStatistics(myIdentityType, appNetworkType) if (err != nil) { statisticsCompleteBoolMutex.Lock() statisticsCompleteBool = true statisticsCompleteBoolMutex.Unlock() pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == false){ setErrorEncounteredPage(window, err, previousPage) } return } statisticsCompleteBoolMutex.Lock() statisticsCompleteBool = true statisticsCompleteBoolMutex.Unlock() pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == false){ setViewMyChatFilterStatisticsPage(window, myIdentityType, true, numberOfConversations, numberOfFilteredConversations, myChatFilterStatisticsItemsList, previousPage) } } page := container.NewVBox(title, backButton, widget.NewSeparator(), loadingTextLabelCentered) setPageContent(page, window) go calculateStatisticsAndRefreshPageFunction() return } myEnabledChatFiltersList, err := myChatFilters.GetAllMyEnabledChatFiltersList(myIdentityType) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } numberOfConversationsString := helpers.ConvertIntToString(numberOfConversations) numberOfFilteredConversationsString := helpers.ConvertIntToString(numberOfFilteredConversations) if (len(myEnabledChatFiltersList) == 0){ noFiltersEnabledDescriptionA := getBoldLabelCentered("You have not enabled any chat filters.") noFiltersEnabledDescriptionB := getLabelCentered("All " + numberOfConversationsString + " of your conversations will be shown.") page := container.NewVBox(title, backButton, widget.NewSeparator(), noFiltersEnabledDescriptionA, noFiltersEnabledDescriptionB) setPageContent(page, window) return } description1 := getLabelCentered("Below are the percentages of conversations that pass your chat filters.") description2 := getLabelCentered("Filtered Conversations divides by the number of conversations you would see without the filter.") getStatisticsDisplayContainer := func()(*fyne.Container, error){ statisticsDisplayContainer := container.NewVBox() for _, chatFilterStatisticsItem := range statisticsItemsList{ filterName := chatFilterStatisticsItem.FilterName numberOfRecipientsWhoPassFilter := chatFilterStatisticsItem.NumberOfRecipientsWhoPassFilter percentageOfRecipientsWhoPassFilter := chatFilterStatisticsItem.PercentageOfRecipientsWhoPassFilter numberOfFilterExcludedRecipients := chatFilterStatisticsItem.NumberOfFilterExcludedRecipients percentageOfFilterExcludedRecipientsWhoPassFilter := chatFilterStatisticsItem.PercentageOfFilterExcludedRecipientsWhoPassFilter filterIsEnabled := slices.Contains(myEnabledChatFiltersList, filterName) if (filterIsEnabled == false){ // If the filter is not enabled, all conversations will pass the filter. // We will not show the filter. continue } getFilterTitle := func()string{ if (filterName == "ShowMyContactsOnly"){ return "Only show conversations with my contacts." } if (filterName == "ShowMyMatchesOnly"){ return "Only show conversations with my matches." } if (filterName == "ShowHasMessagedMeOnly"){ return "Only show conversations with users who have messaged me." } if (filterName == "OnlyShowLikedUsers"){ return "Only show conversations with users I have liked." } if (filterName == "HideIgnoredUsers"){ return "Hide conversations with users I have ignored." } return filterName } filterTitle := getFilterTitle() filterTitleLabel := getItalicLabelCentered(filterTitle) allConversationsLabel := getBoldLabel("All Conversations") filteredConversationsLabel := getBoldLabel("Filtered Conversations") numberOfRecipientsWhoPassFilterString := helpers.ConvertIntToString(numberOfRecipientsWhoPassFilter) percentageOfRecipientsWhoPassFilterString := helpers.ConvertFloat64ToStringRounded(percentageOfRecipientsWhoPassFilter, 2) numberOfFilterExcludedRecipientsString := helpers.ConvertIntToString(numberOfFilterExcludedRecipients) percentageOfFilterExcludedRecipientsWhoPassFilterString := helpers.ConvertFloat64ToStringRounded(percentageOfFilterExcludedRecipientsWhoPassFilter, 2) allConversationsPercentageLabel := getLabelCentered(numberOfRecipientsWhoPassFilterString + "/" + numberOfConversationsString + " = " + percentageOfRecipientsWhoPassFilterString + "%") filteredConversationsPercentageLabel := getLabelCentered(numberOfFilteredConversationsString + "/" + numberOfFilterExcludedRecipientsString + " = " + percentageOfFilterExcludedRecipientsWhoPassFilterString + "%") filterStatisticsDisplayGrid := getContainerCentered(container.NewGridWithColumns(2, allConversationsLabel, filteredConversationsLabel, allConversationsPercentageLabel, filteredConversationsPercentageLabel)) statisticsDisplayContainer.Add(filterTitleLabel) statisticsDisplayContainer.Add(filterStatisticsDisplayGrid) statisticsDisplayContainer.Add(widget.NewSeparator()) } return statisticsDisplayContainer, nil } statisticsDisplayContainer, err := getStatisticsDisplayContainer() if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } statisticsDisplayContainerCentered := statisticsDisplayContainer page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), statisticsDisplayContainerCentered) setPageContent(page, window) }