seekia/gui/chatGui.go

3256 lines
119 KiB
Go

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)
}