From 92422de98ee912f5618503fc79bdabcf52ab5f35 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 14:03:56 -0800 Subject: [PATCH 01/47] Support Custom Tor Configuration Fixes: #19 --- lib/l10n/intl_de.arb | 10 +- lib/l10n/intl_en.arb | 10 +- lib/l10n/intl_es.arb | 10 +- lib/l10n/intl_fr.arb | 10 +- lib/l10n/intl_it.arb | 16 +- lib/l10n/intl_pl.arb | 480 ++++++++++++++++++----------------- lib/l10n/intl_pt.arb | 10 +- lib/l10n/intl_ru.arb | 54 ++-- lib/settings.dart | 52 ++++ lib/themes/midnight.dart | 3 + lib/views/torstatusview.dart | 141 ++++++++-- lib/widgets/textfield.dart | 17 +- 12 files changed, 517 insertions(+), 296 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 5445e690..cb6d0c28 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,14 @@ { "@@locale": "de", - "@@last_modified": "2021-12-20T09:20:03+01:00", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "msgAddToAccept": "Add this account to your contacts in order to accept this file.", "btnSendFile": "Send File", "msgConfirmSend": "Are you sure you want to send", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4a9c34de..da0baec7 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,14 @@ { "@@locale": "en", - "@@last_modified": "2021-12-20T09:20:03+01:00", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "msgAddToAccept": "Add this account to your contacts in order to accept this file.", "btnSendFile": "Send File", "msgConfirmSend": "Are you sure you want to send", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 9b39b7ac..d55072a1 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,14 @@ { "@@locale": "es", - "@@last_modified": "2021-12-20T09:20:03+01:00", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "msgAddToAccept": "Add this account to your contacts in order to accept this file.", "btnSendFile": "Send File", "msgConfirmSend": "Are you sure you want to send", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index a9a983e8..249b781c 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,14 @@ { "@@locale": "fr", - "@@last_modified": "2021-12-20T09:20:03+01:00", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "msgConfirmSend": "Êtes-vous sûr de vouloir envoyer", "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe de", "msgFileTooBig": "La taille du fichier ne peut pas dépasser 10 Go", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 90f26cc6..b903ba34 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,17 @@ { "@@locale": "it", - "@@last_modified": "2021-12-20T09:20:03+01:00", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", + "serverNotSynced": "Sincronizzazione Nuovi Messaggi (L'operazione può richiedere del tempo)...", + "copiedClipboardNotification": "Copiato negli appunti", + "savePeerHistory": "Salva Cronologia ", "blockUnknownLabel": "Blocca Contatti Sconosciuti", "unblockBtn": "Sblocca il Contatto", "dontSavePeerHistory": "Elimina Cronologia", @@ -83,7 +94,6 @@ "peerOfflineMessage": "Il contatto è offline, i messaggi non possono essere recapitati in questo momento", "networkStatusConnecting": "Connessione alla rete e ai contatti...", "savePeerHistoryDescription": "Determina se eliminare la cronologia eventualmente associata al contatto.", - "savePeerHistory": "Salva cronologia ", "profileOnionLabel": "Invia questo indirizzo ai contatti con cui vuoi connetterti", "importLocalServerLabel": "Importa un server ospitato localmente", "importLocalServerButton": "Importa %1", @@ -163,7 +173,6 @@ "tooltipSendFile": "Invia file", "settingFileSharing": "Condivisione file", "descriptionFileSharing": "L'esperimento di condivisione dei file ti consente di inviare e ricevere file dai contatti e dai gruppi di Cwtch. Tieni presente che la condivisione di un file con un gruppo farà sì che i membri di quel gruppo si colleghino con te direttamente su Cwtch per scaricarlo.", - "serverNotSynced": "Sincronizzazione nuovi messaggi (l'operazione può richiedere del tempo)...", "enterCurrentPasswordForDelete": "Inserisci la password attuale per eliminare questo profilo.", "invalidImportString": "Importazione stringa non valida", "tooltipOpenSettings": "Aprire il pannello delle impostazioni", @@ -230,7 +239,6 @@ "blocked": "Bloccato", "search": "Ricerca...", "copyBtn": "Copia", - "copiedClipboardNotification": "Copiato negli Appunti", "newGroupBtn": "Crea un nuovo gruppo", "invitationLabel": "Invito", "serverInfo": "Informazioni sul server", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 04d49c9f..845f5819 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,78 +1,267 @@ { "@@locale": "pl", - "@@last_modified": "2021-12-20T09:20:03+01:00", - "msgAddToAccept": "Add this account to your contacts in order to accept this file.", - "btnSendFile": "Send File", - "msgConfirmSend": "Are you sure you want to send", - "msgFileTooBig": "File size cannot exceed 10 GB", - "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", - "loadingCwtch": "Loading Cwtch...", - "themeColorLabel": "Color Theme", - "themeNameNeon2": "Neon2", - "themeNameNeon1": "Neon1", - "themeNameMidnight": "Midnight", - "themeNameMermaid": "Mermaid", - "themeNamePumpkin": "Pumpkin", - "themeNameGhost": "Ghost", - "themeNameVampire": "Vampire", - "themeNameWitch": "Witch", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", + "largeTextLabel": "Duży", + "settingInterfaceZoom": "Przybliżenie", + "localeDe": "Deutsche", + "localePt": "Portuguesa", + "localeFr": "Frances", + "localeEn": "English", + "settingLanguage": "Język", + "blockUnknownLabel": "Blokuj nieznajomych", + "zoomLabel": "Przybliżenie interfejsu (wpływa głównie na rozmiar tekstu i przycisków)", + "versionBuilddate": "Wersja: %1 Zbudowana: %2", + "cwtchSettingsTitle": "Ustawienia Cwtch", + "unlock": "Odblokuj", + "yourServers": "Twoje serwery", + "yourProfiles": "Twoje profile", + "error0ProfilesLoadedForPassword": "Znaleziono 0 profili z tym hasłem", + "password": "Hasło", + "enterProfilePassword": "Wprowadź hasło aby wyświetlić profile", + "addNewProfileBtn": "Dodaj", + "deleteConfirmText": "USUŃ", + "deleteProfileConfirmBtn": "Usuń", + "deleteConfirmLabel": "Wpisz USUŃ aby potwierdzić", + "deleteProfileBtn": "Usuń profil", + "passwordChangeError": "Zmiana hasła nie powiodła się: hasło niepoprawne", + "passwordErrorMatch": "Hasła nie są identyczne", + "saveProfileBtn": "Zapisz profil", + "createProfileBtn": "Utwórz profil", + "passwordErrorEmpty": "Hasło nie może być puste", + "password2Label": "Powtórz hasło", + "password1Label": "Hasło", + "currentPasswordLabel": "Obecne hasło", + "yourDisplayName": "Nazwa", + "profileOnionLabel": "Send this address to contacts you want to connect with", + "noPasswordWarning": "Brak hasła do konta oznacza, że dane przechowywane na tym urządzeniu nie będą zaszyfrowane", + "radioNoPassword": "Niezaszyfrowany (brak hasła)", + "radioUsePassword": "Hasło", + "copiedToClipboardNotification": "Copied to Clipboard", + "editProfile": "Edytuj profil", + "newProfile": "Nowy profil", + "defaultProfileName": "Nowy profil", + "profileName": "Nazwa", + "editProfileTitle": "Edytuj profil", + "addProfileTitle": "Dodaj nowy profil", + "deleteBtn": "Delete", + "unblockBtn": "Odblokuj", + "dontSavePeerHistory": "Nie", + "savePeerHistoryDescription": "Zapisywanie wiadomości", + "savePeerHistory": "Tak", + "blockBtn": "Zablokuj", + "saveBtn": "Save", + "displayNameLabel": "Nazwa", + "addressLabel": "Adresy", + "puzzleGameBtn": "Puzzle", + "bulletinsBtn": "Biuletyny", + "listsBtn": "Listy", + "chatBtn": "Chat", + "rejectGroupBtn": "Odrzuć", + "acceptGroupBtn": "Akceptuj", + "acceptGroupInviteLabel": "Czy chcesz zaakceptować zaproszenie do grupy", + "newGroupBtn": "Utwórz nową grupę", + "copiedClipboardNotification": "Skopiowano do schowka", + "copyBtn": "Kopiuj", + "peerOfflineMessage": "Znajomy jest niedostępny, nie można dostarczyć wiadomości", + "peerBlockedMessage": "Użytkownik jest zablokowany", + "pendingLabel": "W toku", + "acknowledgedLabel": "OK", + "couldNotSendMsgError": "Wiadomość nie została wysłana", + "dmTooltip": "Kliknij, aby wysłać wiadomość", + "membershipDescription": "Lista użytkowników, którzy wysyłali wiadomości w tej grupie. Członkowie grupy, którzy nie wysyłali żadnych wiadomości nie są na tej liście.", + "addListItemBtn": "Dodaj", + "peerNotOnline": "Znajomy jest niedostępny. Nie można użyć aplikacji.", + "searchList": "Search List", + "update": "Zaktualizuj", + "inviteBtn": "Zaproś", + "inviteToGroupLabel": "Zaproś do grupy", + "groupNameLabel": "Group name", + "viewServerInfo": "Informacje o serwerze", + "serverNotSynced": "Synchronizacja wiadomości (to może chwilę potrwać)...", + "serverSynced": "Zsynchronizowano", + "serverConnectivityDisconnected": "Brak połączenia z serwerem", + "serverConnectivityConnected": "Połączono z serwerem", + "serverInfo": "Informacje o serwerze", + "invitationLabel": "Zaproszenie", + "serverLabel": "Server", + "search": "Szukaj...", + "cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.", + "cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.", + "cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.", + "cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.", + "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", + "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", + "blocked": "Zablokowany", + "pasteAddressToAddContact": "Wklej adres Cwtch znajomego, zaproszenie do grupy albo pęk kluczy", + "titlePlaceholder": "Tytuł...", + "postNewBulletinLabel": "Opublikuj nowy biuletyn", + "newBulletinLabel": "Nowy biuletyn", + "joinGroup": "Dołącz do grupy", + "createGroup": "Utwórz grupę", + "addPeer": "Dodaj znajomego", + "groupAddr": "Adres", + "invitation": "Zaproszenie", + "server": "Serwer", + "groupName": "Nazwa grupy", + "peerName": "Nazwa", + "peerAddress": "Adres", + "joinGroupTab": "Dołącz do grupy", + "createGroupTab": "Utwórz grupę", + "addPeerTab": "Dodaj znajomego", + "createGroupBtn": "Utwórz", + "defaultGroupName": "Nowa grupa", + "createGroupTitle": "Utwórz grupę", + "msgAddToAccept": "Dodaj tego użytkownika do znajomych aby pobrać plik.", + "btnSendFile": "Wyślij plik", + "msgConfirmSend": "Na pewno chcesz wysłać ten plik?", + "msgFileTooBig": "Rozmiar pliku nie może przekraczać 10 GB", + "storageMigrationModalMessage": "Migrowanie profili do nowego formatu. To może chwilę potrwać...", + "loadingCwtch": "Ładowanie Cwtch...", + "themeColorLabel": "Motyw", + "themeNameNeon2": "Neon 2", + "themeNameNeon1": "Neon 1", + "themeNameMidnight": "Północ", + "themeNameMermaid": "Syrena", + "themeNamePumpkin": "Dynia", + "themeNameGhost": "Duch", + "themeNameVampire": "Wampir", + "themeNameWitch": "Czarownica", "themeNameCwtch": "Cwtch", - "settingDownloadFolder": "Download Folder", - "settingImagePreviewsDescription": "Images will be downloaded and previewed automatically. Please note that image previews can often lead to security vulnerabilities, and you should not enable this Experiment if you use Cwtch with untrusted contacts. Profile pictures are planned for Cwtch 1.6.", - "settingImagePreviews": "Image Previews and Profile Pictures", - "experimentClickableLinksDescription": "The clickable links experiment allows you to click on URLs shared in messages", - "enableExperimentClickableLinks": "Enable Clickable Links", - "serverConnectionsLabel": "Connection", - "serverTotalMessagesLabel": "Total Messages", - "serverMetricsLabel": "Server Metrics", - "manageKnownServersShort": "Servers", - "manageKnownServersLong": "Manage Known Servers", - "displayNameTooltip": "Please enter a display name", - "manageKnownServersButton": "Manage Known Servers", - "fieldDescriptionLabel": "Description", - "groupsOnThisServerLabel": "Groups I am in hosted on this server", - "importLocalServerButton": "Import %1", - "importLocalServerSelectText": "Select Local Server", - "importLocalServerLabel": "Import a locally hosted server", - "titleManageServers": "Zarządzaj serwerami", - "leaveGroup": "Wyjdź z tej rozmowy", - "yesLeave": "Tak, wyjdź z tej rozmowy", - "newPassword": "Nowe hasło", - "accepted": "Przyjęte!", - "rejected": "Odrzucone!", + "settingDownloadFolder": "Folder dla pobranych plików", + "settingImagePreviewsDescription": "Automatyczne pobieranie i podgląd obrazów. Pamiętaj, że podgląd obrazów jest potencjalną luką w zabezpieczeniach i nie należy używać tej eksperymentalnej funkcjonalności jeśli używasz Cwtch do komunikacji z niezaufanymi osobami. Zdjęcia profilowe są przewidziane na wersję Cwtch 1.6", + "settingImagePreviews": "Podgląd zdjęć i zdjęcia profilowe", + "experimentClickableLinksDescription": "Klikalne linki (eksperymentalne). Umożliwia klikanie na linki w wiadomościach, aby je otworzyć", + "enableExperimentClickableLinks": "Włącz klikalne linki", + "serverConnectionsLabel": "Połączenie", + "serverTotalMessagesLabel": "Wiadomości łącznie", + "serverMetricsLabel": "Statystyki serwera", + "manageKnownServersShort": "Serwery", + "manageKnownServersLong": "Zarządzaj znanymi serwerami", + "displayNameTooltip": "Wprowadź nazwę", + "manageKnownServersButton": "Zarządzaj znanymi serwerami", + "fieldDescriptionLabel": "Opis", + "groupsOnThisServerLabel": "Grupy na tym serwerze, których jesteś członkiem", + "importLocalServerButton": "Importuj %1", + "importLocalServerSelectText": "Wybierz lokalny serwer", + "importLocalServerLabel": "Importuj lokalnie hostowany serwer", + "fileCheckingStatus": "Sprawdzanie statusu pobierania", + "fileInterrupted": "Przerwano", + "encryptedServerDescription": "Zaszyfrowanie serwera hasłem chroni go przed dostępem innych osób korzystających z tego urządzenia.", + "plainServerDescription": "Zalecamy ustawienie hasła dla Cwtch. Jeśli hasło nie zostanie ustawione, każdy z dostępem do tego urządzenia uzyska dostęp do tego serwera, w tym do wrażliwych kluczy kryptograficznych.", + "deleteServerConfirmBtn": "Usuń", + "deleteServerSuccess": "Usunięto serwer", + "settingServersDescription": "Hostowanie serwerów (eksperymentalne) umożliwia tworzenie i zarządzanie serwerami Cwtch", + "settingServers": "Hostowanie serwerów", + "unlockProfileTip": "Utwórz albo odblokuj profil aby rozpocząć!", + "unlockServerTip": "Utwórz albo odblokuj serwer aby rozpocząć!", + "serverAutostartDescription": "Decyduje, czy aplikacja automatycznie włączy serwer po uruchomieniu", + "serverDescriptionDescription": "Opis serwera widoczny tylko dla Ciebie", + "titleManageProfilesShort": "Profil", + "descriptionFileSharing": "Udostępnianie plików (eksperymentalne) umożliwia wysyłanie i otrzymywanie plików od znajomych i grup. Pamiętaj, że udostępnienie pliku grupie spowoduje bezpośrednie połączenie się członków grupy z Tobą przez Cwtch w celu pobrania pliku.", + "messageFileOffered": "Znajomy chce wysłać Ci plik", + "messageEnableFileSharing": "Włącz udostępnianie plików (eksperymentalne) aby wyświetlić tę wiadomość", + "descriptionStreamerMode": "Ukrywa wrażliwe elementy interfejsu, np. adresy profili i kontaktów, na potrzeby udostępniania swojego ekranu", + "blockUnknownConnectionsEnabledDescription": "Ta konwersacja zostanie usunięta gdy zamkniesz Cwtch! Możesz włączyć zapisywanie wiadomości dla każdej konwersacji osobno w menu w prawym górnym rogu.", + "blockedMessageMessage": "Ta wiadomość jest od użytkownika, który został przez Ciebie zablokowany", + "plainProfileDescription": "Zalecamy ustawienie hasła dla profilu Cwtch. Jeśli hasło nie zostanie ustawione, każdy z dostępem do tego urządzenia uzyska dostęp do tego profilu, w tym do wiadomości, znajomych i wrażliwych kluczy kryptograficznych.", + "encryptedProfileDescription": "Zaszyfrowanie profilu hasłem chroni go przed dostępem innych osób korzystających z tego urządzenia", + "addContactConfirm": "Dodaj znajomego %1", + "addContact": "Dodaj znajomego", + "contactGoto": "Przejdź do konwersacji z %1", + "settingUIColumnOptionSame": "Tak samo jak w przypadku trybu wertykalnego", + "settingUIColumnLandscape": "Kolumny interfejsu w trybie horyzontalnym", + "settingUIColumnPortrait": "Kolumny interfejsu w trybie wertykalnym", + "tooltipRejectContactRequest": "Odrzuć zaproszenie do znajomych", + "tooltipAcceptContactRequest": "Akceptuj zaproszenie do znajomych", + "notificationNewMessageFromPeer": "Nowa wiadomość od znajomego!", + "groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Czaty Grupowe (eksperymentalne) w Ustawieniach", + "shutdownCwtchDialog": "Zamknąć Cwtch? Wszystkie połączenia zostaną zakończone, a aplikacja zostanie zamknięta.", + "malformedMessage": "Wiadomość uszkodzona", + "profileDeleteSuccess": "Profil został usunięty", + "debugLog": "Włącz zapisywanie logów konsoli", + "torNetworkStatus": "Status sieci Tor", + "addContactFirst": "Wybierz lub dodaj znajomego aby rozpocząć konwersację.", + "createProfileToBegin": "Utwórz albo odblokuj profil aby rozpocząć", + "nickChangeSuccess": "Nazwa profilu została zmieniona", + "addServerFirst": "Musisz dodać serwer, aby utworzyć grupę", + "deleteProfileSuccess": "Profil został usunięty", + "sendInvite": "Wyślij zaproszenie do znajomych albo do grupy", "sendAnInvitation": "You sent an invitation for: ", + "contactSuggestion": "This is a contact suggestion for: ", + "rejected": "Odrzucono!", + "accepted": "Zaakceptowano!", + "chatHistoryDefault": "Ta konwersacja zostanie usunięta gdy zamkniesz Cwtch! Możesz włączyć zapisywanie wiadomości dla każdej konwersacji osobno w menu w prawym górnym rogu.", + "yesLeave": "Opuść", + "reallyLeaveThisGroupPrompt": "Na pewno chcesz opuścić tę grupę? Wszystkie wiadomości i atrybuty zostaną usunięte.", + "leaveGroup": "Opuść grupę", + "inviteToGroup": "Zaproszono Cię do grupy:", + "dateNever": "Nigdy", + "dateLastYear": "Rok temu", + "dateYesterday": "Wczoraj", + "dateLastMonth": "Miesiąc temu", + "dateRightNow": "Teraz", + "successfullAddedContact": "Dodano znajomego ", + "descriptionBlockUnknownConnections": "Blokowanie połączeń od osób, które nie są na liście Twoich znajomych.", + "descriptionExperimentsGroups": "Czaty grupowe (eksperymentalne) łączą się z niezaufanymi serwerami, aby umożliwić komunikację grupową.", + "descriptionExperiments": "Funkcje eksperymentalne są opcjonalne. Dodają one funkcjonalności, które mogą być mniej prywatne niż domyślne konwersacje 1:1, np. czaty grupowe, integracja z botami, itp.", + "titleManageProfiles": "Zarządzaj Profilami", + "tooltipUnlockProfiles": "Wprowadź hasło, aby odblokować zaszyfrowane profile.", + "titleManageContacts": "Konwersacje", + "tooltipAddContact": "Dodaj znajomego lub grupę", + "tooltipOpenSettings": "Ustawienia", + "contactAlreadyExists": "Ten znajomy już istnieje", + "invalidImportString": "Invalid import string", + "conversationSettings": "Ustawienia konwersacji", + "enterCurrentPasswordForDelete": "Aby usunąć ten profil, wprowadź hasło.", + "enableGroups": "Włącz czaty grupowe", + "localeIt": "Italiana", + "localeEs": "Espanol", + "todoPlaceholder": "Do zdobienia...", + "addNewItem": "Dodaj do listy", + "addListItem": "Add a New List Item", + "newConnectionPaneTitle": "Nowe połączenie", + "networkStatusOnline": "Połączono", + "networkStatusConnecting": "Łączenie z siecią i znajomymi...", + "networkStatusAttemptingTor": "Próba połączenia z siecią Tor", + "networkStatusDisconnected": "Rozłączono. Sprawdź połączenie z Internetem", + "viewGroupMembershipTooltip": "Wyświetl członków grupy", + "loadingTor": "Ładowanie Tor...", + "smallTextLabel": "Mały", + "defaultScalingText": "Domyślny rozmiar tekstu (skalowanie:", + "builddate": "Zbudowana: %2", + "version": "Wersja %1", + "versionTor": "Wersja %1 , wersja Tor %2", + "experimentsEnabled": "Włącz funkcje eksperymentalne", + "themeDark": "Ciemny", + "themeLight": "Jasny", + "settingTheme": "Motyw", + "titleManageServers": "Zarządzaj serwerami", + "newPassword": "Nowe hasło", "torVersion": "Wersja Tor", "torStatus": "Status Tor", "resetTor": "Reset", "cancel": "Anuluj", "sendMessage": "Wyślij wiadomość", - "sendInvite": "Wyślij kontakt lub zaproszenie do grupy", - "deleteProfileSuccess": "Pomyślnie usunięto profil", - "nickChangeSuccess": "Nick w profilu został zmieniony pomyślnie", - "torNetworkStatus": "Stan sieci Tor", - "debugLog": "Włącz logowanie debugowania konsoli", - "profileDeleteSuccess": "Pomyślnie usunięto profil", - "malformedMessage": "Źle sformatowana wiadomość", "shutdownCwtchTooltip": "Zamknij Cwtch", "shutdownCwtchDialogTitle": "Zamknąć Cwtch?", "shutdownCwtchAction": "Zamknij Cwtch", "tooltipShowPassword": "Pokaż hasło", "tooltipHidePassword": "Ukryj hasło", - "notificationNewMessageFromPeer": "Nowa wiadomość od kontaktu!", "notificationNewMessageFromGroup": "Nowa wiadomość w grupie!", - "tooltipAcceptContactRequest": "Zaakceptuj tę prośbę o kontakt.", - "tooltipRejectContactRequest": "Odrzuć tę prośbę o kontakt", "tooltipReplyToThisMessage": "Odpowiedz na tę wiadomość", "tooltipRemoveThisQuotedMessage": "Usuń cytowaną wiadomość.", "settingUIColumnDouble12Ratio": "Podwójny (1:2)", "settingUIColumnSingle": "Pojedynczy", "settingUIColumnDouble14Ratio": "Podwójny (1:4)", - "settingUIColumnOptionSame": "Tak samo jak w przypadku trybu portretowego", - "contactGoto": "Przejdź do rozmowy z %1", - "addContact": "Dodaj kontakt", - "addContactConfirm": "Dodaj kontakt %1", "placeholderEnterMessage": "Wpisz wiadomość...", - "blockedMessageMessage": "Ta wiadomość pochodzi z profilu, który został przez Ciebie zablokowany.", "showMessageButton": "Pokaż wiadomość", "addServerTitle": "Dodaj serwer", "editServerTitle": "Edytuj serwer", @@ -86,27 +275,14 @@ "serversManagerTitleShort": "Serwery", "addServerTooltip": "Dodaj nowy serwer", "enterServerPassword": "Wprowadź hasło, aby odblokować serwer", - "settingServers": "Hosting serwerów", "enterCurrentPasswordForDeleteServer": "Wprowadź aktualne hasło, aby usunąć ten serwer", "newMessagesLabel": "Nowe wiadomości", "localePl": "Polski", "localeRU": "Rosyjski", "copyAddress": "Skopiuj adres", - "deleteServerSuccess": "Pomyślnie usunięto serwer", - "deleteServerConfirmBtn": "Naprawdę usuń serwer", "fileSavedTo": "Zapisano do", - "fileCheckingStatus": "Sprawdzanie stanu pobierania", "verfiyResumeButton": "Zweryfikuj\/wznów", "copyServerKeys": "Kopiuj klucze", - "fileInterrupted": "Przerwane", - "encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", - "plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.", - "settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers", - "unlockProfileTip": "Please create or unlock a profile to begin!", - "unlockServerTip": "Please create or unlock a server to begin!", - "serverAutostartDescription": "Controls if the application will automatically launch the server on start", - "serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared", - "blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach", "archiveConversation": "Zarchiwizuj tę rozmowę", "streamerModeLabel": "Tryb streamera\/prezentacji", "retrievingManifestMessage": "Pobieranie informacji o pliku...", @@ -114,175 +290,7 @@ "downloadFileButton": "Pobierz", "labelFilename": "Nazwa pliku", "labelFilesize": "Rozmiar", - "messageEnableFileSharing": "Włącz eksperyment udostępniania plików, aby wyświetlić tę wiadomość.", "messageFileSent": "Plik został wysłany", - "messageFileOffered": "Kontakt proponuje wysłanie Ci pliku", "tooltipSendFile": "Wyślij plik", - "settingFileSharing": "Udostępnianie plików", - "descriptionFileSharing": "Eksperyment udostępniania plików pozwala na wysyłanie i odbieranie plików od kontaktów i grup Cwtch. Zauważ, że udostępnienie pliku grupie spowoduje, że członkowie tej grupy połączą się z Tobą bezpośrednio przez Cwtch, aby go pobrać.", - "titleManageProfilesShort": "Profile", - "descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses", - "plainProfileDescription": "We recommend that you protect your Cwtch profiles with a password. If you do not set a password on this profile then anyone who has access to this device may be able to access information about this profile, including contacts, messages and sensitive cryptographic keys.", - "encryptedProfileDescription": "Encrypting a profile with a password protects it from other people who may also use this device. Encrypted profiles cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.", - "settingUIColumnLandscape": "UI Columns in Landscape Mode", - "settingUIColumnPortrait": "UI Columns in Portrait Mode", - "groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", - "shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", - "addContactFirst": "Add or pick a contact to begin chatting.", - "createProfileToBegin": "Please create or unlock a profile to begin", - "addServerFirst": "You need to add a server before you can create a group", - "contactSuggestion": "This is a contact suggestion for: ", - "chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", - "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", - "inviteToGroup": "You have been invited to join a group:", - "dateNever": "Never", - "dateLastYear": "Last Year", - "dateYesterday": "Yesterday", - "dateLastMonth": "Last Month", - "dateRightNow": "Right Now", - "successfullAddedContact": "Successfully added ", - "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", - "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", - "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", - "titleManageProfiles": "Manage Cwtch Profiles", - "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", - "titleManageContacts": "Conversations", - "tooltipAddContact": "Add a new contact or conversation", - "tooltipOpenSettings": "Open the settings pane", - "contactAlreadyExists": "Contact Already Exists", - "invalidImportString": "Invalid import string", - "conversationSettings": "Conversation Settings", - "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", - "enableGroups": "Enable Group Chat", - "localeIt": "Italiana", - "localeEs": "Espanol", - "todoPlaceholder": "Todo...", - "addNewItem": "Add a new item to the list", - "addListItem": "Add a New List Item", - "newConnectionPaneTitle": "New Connection", - "networkStatusOnline": "Online", - "networkStatusConnecting": "Connecting to network and contacts...", - "networkStatusAttemptingTor": "Attempting to connect to Tor network", - "networkStatusDisconnected": "Disconnected from the internet, check your connection", - "viewGroupMembershipTooltip": "View Group Membership", - "loadingTor": "Loading tor...", - "smallTextLabel": "Small", - "defaultScalingText": "Default size text (scale factor:", - "builddate": "Built on: %2", - "version": "Version %1", - "versionTor": "Version %1 with tor %2", - "experimentsEnabled": "Enable Experiments", - "themeDark": "Dark", - "themeLight": "Light", - "settingTheme": "Theme", - "largeTextLabel": "Large", - "settingInterfaceZoom": "Zoom level", - "localeDe": "Deutsche", - "localePt": "Portuguesa", - "localeFr": "Frances", - "localeEn": "English", - "settingLanguage": "Language", - "blockUnknownLabel": "Block Unknown Contacts", - "zoomLabel": "Interface zoom (mostly affects text and button sizes)", - "versionBuilddate": "Version: %1 Built on: %2", - "cwtchSettingsTitle": "Cwtch Settings", - "unlock": "Unlock", - "yourServers": "Your Servers", - "yourProfiles": "Your Profiles", - "error0ProfilesLoadedForPassword": "0 profiles loaded with that password", - "password": "Password", - "enterProfilePassword": "Enter a password to view your profiles", - "addNewProfileBtn": "Add new profile", - "deleteConfirmText": "DELETE", - "deleteProfileConfirmBtn": "Really Delete Profile", - "deleteConfirmLabel": "Type DELETE to confirm", - "deleteProfileBtn": "Delete Profile", - "passwordChangeError": "Error changing password: Supplied password rejected", - "passwordErrorMatch": "Passwords do not match", - "saveProfileBtn": "Save Profile", - "createProfileBtn": "Create Profile", - "passwordErrorEmpty": "Password cannot be empty", - "password2Label": "Reenter password", - "password1Label": "Password", - "currentPasswordLabel": "Current Password", - "yourDisplayName": "Your Display Name", - "profileOnionLabel": "Send this address to contacts you want to connect with", - "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", - "radioNoPassword": "Unencrypted (No password)", - "radioUsePassword": "Password", - "copiedToClipboardNotification": "Copied to Clipboard", - "editProfile": "Edit Profille", - "newProfile": "New Profile", - "defaultProfileName": "Alice", - "profileName": "Display name", - "editProfileTitle": "Edit Profile", - "addProfileTitle": "Add new profile", - "deleteBtn": "Delete", - "unblockBtn": "Unblock Contact", - "dontSavePeerHistory": "Delete History", - "savePeerHistoryDescription": "Determines whether to delete any history associated with the contact.", - "savePeerHistory": "Save History", - "blockBtn": "Block Contact", - "saveBtn": "Save", - "displayNameLabel": "Display Name", - "addressLabel": "Address", - "puzzleGameBtn": "Puzzle Game", - "bulletinsBtn": "Bulletins", - "listsBtn": "Lists", - "chatBtn": "Chat", - "rejectGroupBtn": "Reject", - "acceptGroupBtn": "Accept", - "acceptGroupInviteLabel": "Do you want to accept the invitation to", - "newGroupBtn": "Create new group", - "copiedClipboardNotification": "Copied to clipboard", - "copyBtn": "Copy", - "peerOfflineMessage": "Contact is offline, messages can't be delivered right now", - "peerBlockedMessage": "Contact is blocked", - "pendingLabel": "Pending", - "acknowledgedLabel": "Acknowledged", - "couldNotSendMsgError": "Could not send this message", - "dmTooltip": "Click to DM", - "membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.", - "addListItemBtn": "Add Item", - "peerNotOnline": "Contact is offline. Applications cannot be used right now.", - "searchList": "Search List", - "update": "Update", - "inviteBtn": "Invite", - "inviteToGroupLabel": "Invite to group", - "groupNameLabel": "Group name", - "viewServerInfo": "Server Info", - "serverNotSynced": "Syncing New Messages (This can take some time)...", - "serverSynced": "Synced", - "serverConnectivityDisconnected": "Server Disconnected", - "serverConnectivityConnected": "Server Connected", - "serverInfo": "Server Information", - "invitationLabel": "Invitation", - "serverLabel": "Server", - "search": "Search...", - "cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.", - "cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.", - "cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.", - "cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.", - "cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.", - "cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.", - "blocked": "Blocked", - "pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation", - "titlePlaceholder": "title...", - "postNewBulletinLabel": "Post new bulletin", - "newBulletinLabel": "New Bulletin", - "joinGroup": "Join group", - "createGroup": "Create group", - "addPeer": "Add Contact", - "groupAddr": "Address", - "invitation": "Invitation", - "server": "Server", - "groupName": "Group name", - "peerName": "Name", - "peerAddress": "Address", - "joinGroupTab": "Join a group", - "createGroupTab": "Create a group", - "addPeerTab": "Add a contact", - "createGroupBtn": "Create", - "defaultGroupName": "Awesome Group", - "createGroupTitle": "Create Group" + "settingFileSharing": "Udostępnianie plików" } \ No newline at end of file diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 8dc3b445..33330404 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,14 @@ { "@@locale": "pt", - "@@last_modified": "2021-12-20T09:20:03+01:00", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "msgAddToAccept": "Add this account to your contacts in order to accept this file.", "btnSendFile": "Send File", "msgConfirmSend": "Are you sure you want to send", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 3887188f..01ee7847 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,13 +1,36 @@ { "@@locale": "ru", - "@@last_modified": "2021-12-20T09:20:03+01:00", - "msgAddToAccept": "Add this account to your contacts in order to accept this file.", - "btnSendFile": "Send File", - "msgConfirmSend": "Are you sure you want to send", - "msgFileTooBig": "File size cannot exceed 10 GB", - "storageMigrationModalMessage": "Migrating profiles to new storage format. This could take a few minutes...", - "loadingCwtch": "Loading Cwtch...", - "themeColorLabel": "Тема", + "@@last_modified": "2022-01-12T22:25:26+01:00", + "msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.", + "btnSendFile": "Отправить файл", + "msgConfirmSend": "Вы уверены, что хотите отправить?", + "msgFileTooBig": "Размер файла не должен превышать 10GB", + "storageMigrationModalMessage": "Перенос профилей в новый формат хранения. Это может занять несколько минут...", + "loadingCwtch": "Загрузка Cwtch...", + "themeColorLabel": "Светлая или Темная тема", + "settingDownloadFolder": "Папка для скачивания", + "serverConnectionsLabel": "Всего соединений:", + "serverTotalMessagesLabel": "Всего сообщений:", + "plainServerDescription": "Мы настоятельно рекомендуем защитить свой Onion сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.", + "settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер для Cwtch в скрытой сети Tor. В меню появится дополнительная опция Серверы", + "streamerModeLabel": "Режим маскировки", + "settingUIColumnSingle": "Один стобец", + "settingUIColumnLandscape": "Столбцы чатов в ландшафтном режиме", + "settingUIColumnPortrait": "Столбцы чатов в портретном режиме", + "resetTor": "Сброс", + "descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей, не состоящих в ваших контактах будут отклонены.", + "descriptionExperimentsGroups": "Данная экспериментальная функция позволяет создавать группы в Cwtch, чтобы облегчить Вам общение с более чем одним контактом. Для создания групп необходимо включить функцию создания сервера и создать сервер в главном меню программы.", + "descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..", + "settingLanguage": "Выбрать язык", + "profileName": "Введите имя...", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", + "torSettingsCustomControlPort": "Custom Control Port", + "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", + "torSettingsCustomSocksPort": "Custom SOCKS Port", + "torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service", + "torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration", "themeNameNeon2": "Неон2", "themeNameNeon1": "Неон1", "themeNameMidnight": "Полночь", @@ -17,13 +40,10 @@ "themeNameVampire": "Вампир", "themeNameWitch": "Ведьма", "themeNameCwtch": "Cwtch", - "settingDownloadFolder": "Скачать папку", "settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами. Аватары профиля запланированы для версии Cwtch 1.6.", "settingImagePreviews": "Предпросмотр изображений и фотографий профиля", "experimentClickableLinksDescription": "Экспериментальная функция которая позволяет нажимать на URL адреса в сообщениях", "enableExperimentClickableLinks": "Включить кликабельные ссылки", - "serverConnectionsLabel": "Соединение", - "serverTotalMessagesLabel": "Всего сообщений", "serverMetricsLabel": "Показатели сервера", "manageKnownServersShort": "Серверы", "manageKnownServersLong": "Управление серверами", @@ -53,12 +73,10 @@ "fileInterrupted": "Прервано", "fileSavedTo": "Сохранить в", "encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.", - "plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.", "deleteServerConfirmBtn": "Точно удалить сервер?", "deleteServerSuccess": "Сервер успешно удален", "enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его", "copyAddress": "Копировать адрес", - "settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер Cwtch. В меню появится дополнительная опция Серверы", "settingServers": "Использовать серверы", "enterServerPassword": "Введите пароль для разблокировки сервера", "unlockProfileTip": "Создайте или разблокируйте профиль, чтобы начать", @@ -89,7 +107,6 @@ "openFolderButton": "Открыть папку", "retrievingManifestMessage": "Получение информации о файле...", "descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена", - "streamerModeLabel": "Режим презентации", "archiveConversation": "Отправить чат в архив", "blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках", "showMessageButton": "Показать сообщения", @@ -103,9 +120,6 @@ "settingUIColumnOptionSame": "Как в настройках портретного режима", "settingUIColumnDouble14Ratio": "Двойной (1:4)", "settingUIColumnDouble12Ratio": "Двойной (1:2)", - "settingUIColumnSingle": "Одиночный", - "settingUIColumnLandscape": "UI столбцы в Ландшафтном Режиме", - "settingUIColumnPortrait": "UI столбцы в Портретном режиме", "localePl": "Польский", "tooltipRemoveThisQuotedMessage": "Удалить цитируемое сообщение.", "tooltipReplyToThisMessage": "Ответить на это сообщение", @@ -132,7 +146,6 @@ "sendInvite": "Отправить контакт или приглашение в группу", "sendMessage": "Отправить сообщение", "cancel": "Отмена", - "resetTor": "Сбросс", "torStatus": "Статус Tor", "torVersion": "Версия Tor", "sendAnInvitation": "Вы отправили приглашение для: ", @@ -152,9 +165,6 @@ "dateLastMonth": "Прошлый месяц", "dateRightNow": "Прямо сейчас", "successfullAddedContact": "Успешно добавлен", - "descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей не состоящих в ваших контактах будут отклонены.", - "descriptionExperimentsGroups": "Данная экспериментальная функция позволяет Cwtch подключаться к недоверенной серверной инфраструктуре, чтобы облегчить Вам общение с более чем одним контактом.", - "descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..", "titleManageProfiles": "Управление профилями Cwtch", "tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.", "titleManageContacts": "Разговоры", @@ -191,7 +201,6 @@ "localePt": "Португальский", "localeFr": "Французский", "localeEn": "Английский", - "settingLanguage": "Язык", "blockUnknownLabel": "Блокировать неизвестные контакты", "zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)", "versionBuilddate": "Версия: %1 Сборка от: %2", @@ -221,7 +230,6 @@ "radioUsePassword": "Пароль", "editProfile": "Изменить профиль", "newProfile": "Новый профиль", - "profileName": "Отображаемое имя", "editProfileTitle": "Изменить профиль", "addProfileTitle": "Добавить новый профиль", "unblockBtn": "Разблокировать контакт", diff --git a/lib/settings.dart b/lib/settings.dart index 05d2de03..343a374d 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -39,6 +39,13 @@ class Settings extends ChangeNotifier { bool streamerMode = false; String _downloadPath = ""; + bool _allowAdvancedTorConfig = false; + bool _useCustomTorConfig = false; + String _customTorConfig = ""; + int _socksPort = -1; + int _controlPort = -1; + String _customTorAuth = ""; + void setTheme(String themeId, String mode) { theme = getTheme(themeId, mode); notifyListeners(); @@ -86,6 +93,13 @@ class Settings extends ChangeNotifier { // auto-download folder _downloadPath = settings["DownloadPath"] ?? ""; + // allow a custom tor config + _allowAdvancedTorConfig = settings["AllowAdvancedTorConfig"] ?? false; + _useCustomTorConfig = settings["UseCustomTorrc"] ?? false; + _customTorConfig = settings["CustomTorrc"] ?? ""; + _socksPort = settings["CustomSocksPort"] ?? -1; + _controlPort = settings["CustomControlPort"] ?? -1; + // Push the experimental settings to Consumers of Settings notifyListeners(); } @@ -232,6 +246,38 @@ class Settings extends ChangeNotifier { notifyListeners(); } + bool get allowAdvancedTorConfig => _allowAdvancedTorConfig; + set allowAdvancedTorConfig(bool torConfig) { + _allowAdvancedTorConfig = torConfig; + notifyListeners(); + } + + // Settings / Gettings for setting the custom tor config.. + String get torConfig => _customTorConfig; + set torConfig(String torConfig) { + _customTorConfig = torConfig; + notifyListeners(); + } + + int get socksPort => _socksPort; + set socksPort(int newSocksPort) { + _socksPort = newSocksPort; + notifyListeners(); + } + + int get controlPort => _controlPort; + set controlPort(int controlPort) { + _controlPort = controlPort; + notifyListeners(); + } + + // Setters / Getters for toggling whether the app should use a custom tor config + bool get useCustomTorConfig => _useCustomTorConfig; + set useCustomTorConfig(bool useCustomTorConfig) { + _useCustomTorConfig = useCustomTorConfig; + notifyListeners(); + } + /// Construct a default settings object. Settings(this.locale, this.theme); @@ -252,6 +298,12 @@ class Settings extends ChangeNotifier { "UIColumnModePortrait": uiColumnModePortrait.toString(), "UIColumnModeLandscape": uiColumnModeLandscape.toString(), "DownloadPath": _downloadPath, + "AllowAdvancedTorConfig": _allowAdvancedTorConfig, + "CustomTorRc": _customTorConfig, + "UseCustomTorrc": _useCustomTorConfig, + "CustomSocksPort": _socksPort, + "CustomControlPort": _controlPort, + "CustomAuth": _customTorAuth, }; } } diff --git a/lib/themes/midnight.dart b/lib/themes/midnight.dart index 96841404..17a27176 100644 --- a/lib/themes/midnight.dart +++ b/lib/themes/midnight.dart @@ -39,6 +39,8 @@ class MidnightDark extends CwtchDark { get messageFromMeTextColor => font; //whiteishPurple; get messageFromOtherBackgroundColor => peerBubble; //deepPurple; get messageFromOtherTextColor => font; //whiteishPurple; + get textfieldBackgroundColor => peerBubble; + get textfieldBorderColor => userBubble; } class MidnightLight extends CwtchLight { @@ -66,4 +68,5 @@ class MidnightLight extends CwtchLight { get messageFromMeTextColor => font; //mainTextColor; get messageFromOtherBackgroundColor => peerBubble; //purple; get messageFromOtherTextColor => font; //darkPurple; + get textfieldBackgroundColor => userBubble; } diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 0c6264fd..3ec5e2bc 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -1,3 +1,6 @@ +import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/settings.dart'; +import 'package:cwtch/widgets/textfield.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/torstatus.dart'; import 'package:cwtch/widgets/tor_icon.dart'; @@ -5,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; +import 'globalsettingsview.dart'; /// Tor Status View provides all info on Tor network state and the (future) ability to configure the network in a variety /// of ways (restart, enable bridges, enable pluggable transports etc) @@ -14,6 +18,10 @@ class TorStatusView extends StatefulWidget { } class _TorStatusView extends State { + TextEditingController torSocksPortController = TextEditingController(); + TextEditingController torControlPortController = TextEditingController(); + TextEditingController torConfigController = TextEditingController(); + @override void dispose() { super.dispose(); @@ -30,33 +38,114 @@ class _TorStatusView extends State { } Widget _buildSettingsList() { - return Consumer(builder: (context, torStatus, child) { - return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { - return Scrollbar( - isAlwaysShown: true, - child: SingleChildScrollView( - clipBehavior: Clip.antiAlias, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Column(children: [ - ListTile( - leading: TorIcon(), - title: Text(AppLocalizations.of(context)!.torStatus), - subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : torStatus.status), - trailing: ElevatedButton( - child: Text(AppLocalizations.of(context)!.resetTor), - onPressed: () { - Provider.of(context, listen: false).cwtch.ResetTor(); - }, + return Consumer(builder: ( + context, + settings, + child, + ) { + // We don't want these to update on edit...only on construction... + if (torSocksPortController.text.isEmpty) { + torConfigController.text = settings.torConfig; + torSocksPortController.text = settings.socksPort.toString(); + torControlPortController.text = settings.controlPort.toString(); + } + return Consumer(builder: (context, torStatus, child) { + return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { + return Scrollbar( + isAlwaysShown: true, + child: SingleChildScrollView( + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Column(children: [ + ListTile( + leading: TorIcon(), + title: Text(AppLocalizations.of(context)!.torStatus), + subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : torStatus.status), + trailing: ElevatedButton( + child: Text(AppLocalizations.of(context)!.resetTor), + onPressed: () { + Provider.of(context, listen: false).cwtch.ResetTor(); + }, + ), ), - ), - ListTile( - title: Text(AppLocalizations.of(context)!.torVersion), - subtitle: SelectableText(torStatus.version), - ), - ])))); + ListTile( + title: Text(AppLocalizations.of(context)!.torVersion), + subtitle: SelectableText(torStatus.version), + leading: Icon(CwtchIcons.info_24px, color: settings.current().mainTextColor), + ), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvanced), + subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvancedDescription), + value: settings.allowAdvancedTorConfig, + onChanged: (bool value) { + settings.allowAdvancedTorConfig = value; + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(CwtchIcons.settings_24px, color: settings.current().mainTextColor), + ), + Visibility( + visible: settings.allowAdvancedTorConfig, + child: Column(children: [ + ListTile( + title: Text(AppLocalizations.of(context)!.torSettingsCustomSocksPort), + subtitle: Text(AppLocalizations.of(context)!.torSettingsCustomSocksPortDescription), + leading: Icon(CwtchIcons.swap_horiz_24px, color: settings.current().mainTextColor), + trailing: Container( + width: 100, + child: CwtchTextField( + controller: torSocksPortController, + onChanged: (String socksPort) { + try { + settings.socksPort = int.parse(socksPort); + saveSettings(context); + } catch (e) {} + }, + ))), + ListTile( + title: Text(AppLocalizations.of(context)!.torSettingsCustomControlPort), + subtitle: Text(AppLocalizations.of(context)!.torSettingsCustomControlPortDescription), + leading: Icon(CwtchIcons.swap_horiz_24px, color: settings.current().mainTextColor), + trailing: Container( + width: 100, + child: CwtchTextField( + controller: torControlPortController, + onChanged: (String controlPort) { + try { + settings.controlPort = int.parse(controlPort); + saveSettings(context); + } catch (e) {} + }, + ))), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.torSettingsUseCustomTorServiceConfiguration, style: TextStyle(color: settings.current().mainTextColor)), + subtitle: Text(AppLocalizations.of(context)!.torSettingsUseCustomTorServiceConfigurastionDescription), + value: settings.useCustomTorConfig, + onChanged: (bool value) { + settings.useCustomTorConfig = value; + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(CwtchIcons.enable_experiments, color: settings.current().mainTextColor), + ), + Visibility(visible: settings.useCustomTorConfig, child: Padding( + padding: EdgeInsets.all(5), + child: CwtchTextField( + controller: torConfigController, + multiLine: true, + onChanged: (torConfig) { + settings.torConfig = torConfig; + saveSettings(context); + }, + ))) + ])) + ])))); + }); }); }); } diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index 436bea19..ca98ca57 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../settings.dart'; @@ -7,12 +8,14 @@ doNothing(String x) {} // Provides a styled Text Field for use in Form Widgets. // Callers must provide a text controller, label helper text and a validator. class CwtchTextField extends StatefulWidget { - CwtchTextField({required this.controller, required this.hintText, this.validator, this.autofocus = false, this.onChanged = doNothing}); + CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false}); final TextEditingController controller; final String hintText; final FormFieldValidator? validator; final Function(String) onChanged; final bool autofocus; + final bool multiLine; + final bool number; @override _CwtchTextFieldState createState() => _CwtchTextFieldState(); @@ -20,9 +23,11 @@ class CwtchTextField extends StatefulWidget { class _CwtchTextFieldState extends State { late final FocusNode _focusNode; - + late final ScrollController _scrollController; @override void initState() { + _scrollController = ScrollController(); + _focusNode = FocusNode(); _focusNode.addListener(() { // Select all... @@ -39,6 +44,14 @@ class _CwtchTextFieldState extends State { validator: widget.validator, onChanged: widget.onChanged, autofocus: widget.autofocus, + textAlign: widget.number ? TextAlign.end : TextAlign.start, + keyboardType: widget.multiLine + ? TextInputType.multiline + : widget.number + ? TextInputType.number + : TextInputType.text, + maxLines: widget.multiLine ? null : 1, + scrollController: _scrollController, enableIMEPersonalizedLearning: false, focusNode: _focusNode, decoration: InputDecoration( From 4cdbb042438b52f4572ec8d8816df447bcd7602c Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 14:07:48 -0800 Subject: [PATCH 02/47] Update Translations --- lib/l10n/intl_de.arb | 6 +++--- lib/l10n/intl_en.arb | 8 ++++---- lib/l10n/intl_es.arb | 6 +++--- lib/l10n/intl_fr.arb | 6 +++--- lib/l10n/intl_it.arb | 6 +++--- lib/l10n/intl_pl.arb | 6 +++--- lib/l10n/intl_pt.arb | 8 ++++---- lib/l10n/intl_ru.arb | 6 +++--- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index cb6d0c28..d225aa56 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,8 +1,8 @@ { "@@locale": "de", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index da0baec7..2d63548b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,8 +1,9 @@ { "@@locale": "en", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "settingTheme": "Use Light Themes", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", @@ -196,7 +197,6 @@ "versionTor": "Version %1 with tor %2", "themeDark": "Dark", "themeLight": "Light", - "settingTheme": "Theme", "largeTextLabel": "Large", "settingInterfaceZoom": "Zoom level", "localeDe": "Deutsche", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index d55072a1..300ee131 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,8 +1,8 @@ { "@@locale": "es", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 249b781c..9164448d 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,8 +1,8 @@ { "@@locale": "fr", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index b903ba34..e60052ad 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,8 +1,8 @@ { "@@locale": "it", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 845f5819..52487b41 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,8 +1,8 @@ { "@@locale": "pl", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 33330404..ac96f461 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,8 +1,8 @@ { "@@locale": "pt", - "@@last_modified": "2022-01-12T22:25:26+01:00", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "@@last_modified": "2022-01-12T22:53:15+01:00", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", @@ -182,7 +182,7 @@ "experimentsEnabled": "Enable Experiments", "themeDark": "Dark", "themeLight": "Light", - "settingTheme": "Theme", + "settingTheme": "Use Light Themes", "largeTextLabel": "Grande", "settingInterfaceZoom": "Zoom level", "localeDe": "Deutsche", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 01ee7847..255c4b12 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,6 @@ { "@@locale": "ru", - "@@last_modified": "2022-01-12T22:25:26+01:00", + "@@last_modified": "2022-01-12T22:53:15+01:00", "msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.", "btnSendFile": "Отправить файл", "msgConfirmSend": "Вы уверены, что хотите отправить?", @@ -23,8 +23,8 @@ "descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..", "settingLanguage": "Выбрать язык", "profileName": "Введите имя...", - "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you doing.", - "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc) ", + "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", + "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", "torSettingsCustomControlPort": "Custom Control Port", "torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy", From 958be3e8f7f12f41b6949197bd13431d15614738 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 14:41:17 -0800 Subject: [PATCH 03/47] Upgade lcg --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- lib/views/torstatusview.dart | 22 ++++++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 5953cbf1..270b0601 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-07-16-29-v1.5.4 \ No newline at end of file +2022-01-12-17-26-v1.5.4-3-gaf47036 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index c356452f..68121b4a 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-07-21-30-v1.5.4 \ No newline at end of file +2022-01-12-22-26-v1.5.4-3-gaf47036 \ No newline at end of file diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 3ec5e2bc..c94a8a42 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -133,16 +133,18 @@ class _TorStatusView extends State { inactiveTrackColor: settings.theme.defaultButtonDisabledColor, secondary: Icon(CwtchIcons.enable_experiments, color: settings.current().mainTextColor), ), - Visibility(visible: settings.useCustomTorConfig, child: Padding( - padding: EdgeInsets.all(5), - child: CwtchTextField( - controller: torConfigController, - multiLine: true, - onChanged: (torConfig) { - settings.torConfig = torConfig; - saveSettings(context); - }, - ))) + Visibility( + visible: settings.useCustomTorConfig, + child: Padding( + padding: EdgeInsets.all(5), + child: CwtchTextField( + controller: torConfigController, + multiLine: true, + onChanged: (torConfig) { + settings.torConfig = torConfig; + saveSettings(context); + }, + ))) ])) ])))); }); From 26f32a0790fcd7e07933d77a4dac8c1c5131661f Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 15:15:58 -0800 Subject: [PATCH 04/47] Update Translations + Error Reporting --- lib/l10n/intl_de.arb | 3 +- lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_es.arb | 3 +- lib/l10n/intl_fr.arb | 3 +- lib/l10n/intl_it.arb | 3 +- lib/l10n/intl_pl.arb | 3 +- lib/l10n/intl_pt.arb | 3 +- lib/l10n/intl_ru.arb | 3 +- lib/views/torstatusview.dart | 53 ++++++++++++++++++++++++++++++++---- lib/widgets/textfield.dart | 6 ++++ 10 files changed, 69 insertions(+), 14 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index d225aa56..05071e96 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,7 @@ { "@@locale": "de", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2d63548b..ff0be443 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,7 @@ { "@@locale": "en", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "settingTheme": "Use Light Themes", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 300ee131..9c928936 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,7 @@ { "@@locale": "es", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 9164448d..3e90f9fd 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,7 @@ { "@@locale": "fr", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index e60052ad..b392a0ff 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,7 @@ { "@@locale": "it", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 52487b41..80ebcac5 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,7 @@ { "@@locale": "pl", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index ac96f461..31ce6b76 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,7 @@ { "@@locale": "pt", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", "torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 255c4b12..dc524e28 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,7 @@ { "@@locale": "ru", - "@@last_modified": "2022-01-12T22:53:15+01:00", + "@@last_modified": "2022-01-12T23:54:51+01:00", + "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.", "btnSendFile": "Отправить файл", "msgConfirmSend": "Вы уверены, что хотите отправить?", diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index c94a8a42..1a63899c 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -96,13 +96,33 @@ class _TorStatusView extends State { subtitle: Text(AppLocalizations.of(context)!.torSettingsCustomSocksPortDescription), leading: Icon(CwtchIcons.swap_horiz_24px, color: settings.current().mainTextColor), trailing: Container( - width: 100, + width: 300, child: CwtchTextField( + number: true, controller: torSocksPortController, + validator: (value) { + try { + var port = int.parse(value); + if (port > 0 && port < 65536) { + return null; + } else { + return AppLocalizations.of( + context)! + .torSettingsErrorSettingPort; + } + }catch (e) { + return AppLocalizations.of( + context)! + .torSettingsErrorSettingPort; + } + }, onChanged: (String socksPort) { try { - settings.socksPort = int.parse(socksPort); - saveSettings(context); + var port = int.parse(socksPort); + if (port > 0 && port < 65536) { + settings.socksPort = int.parse(socksPort); + saveSettings(context); + } } catch (e) {} }, ))), @@ -111,13 +131,34 @@ class _TorStatusView extends State { subtitle: Text(AppLocalizations.of(context)!.torSettingsCustomControlPortDescription), leading: Icon(CwtchIcons.swap_horiz_24px, color: settings.current().mainTextColor), trailing: Container( - width: 100, + width: 300, child: CwtchTextField( + number: true, controller: torControlPortController, + validator: (value) { + try { + var port = int.parse(value); + if (port > 0 && port < 65536) { + return null; + } else { + return AppLocalizations.of( + context)! + .torSettingsErrorSettingPort; + } + }catch (e) { + return AppLocalizations.of( + context)! + .torSettingsErrorSettingPort; + } + }, onChanged: (String controlPort) { try { - settings.controlPort = int.parse(controlPort); - saveSettings(context); + var port = int.parse(controlPort); + if (port > 0 && port < 65536) { + settings.controlPort = + int.parse(controlPort); + saveSettings(context); + } } catch (e) {} }, ))), diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index ca98ca57..9f5ff620 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -44,17 +44,22 @@ class _CwtchTextFieldState extends State { validator: widget.validator, onChanged: widget.onChanged, autofocus: widget.autofocus, + autovalidateMode: AutovalidateMode.onUserInteraction, textAlign: widget.number ? TextAlign.end : TextAlign.start, keyboardType: widget.multiLine ? TextInputType.multiline : widget.number ? TextInputType.number : TextInputType.text, + inputFormatters: widget.number ? [ + FilteringTextInputFormatter.digitsOnly + ] : null, maxLines: widget.multiLine ? null : 1, scrollController: _scrollController, enableIMEPersonalizedLearning: false, focusNode: _focusNode, decoration: InputDecoration( + errorMaxLines: 2, hintText: widget.hintText, floatingLabelBehavior: FloatingLabelBehavior.never, filled: true, @@ -64,6 +69,7 @@ class _CwtchTextFieldState extends State { errorStyle: TextStyle( color: theme.current().textfieldErrorColor, fontWeight: FontWeight.bold, + overflow: TextOverflow.visible ), fillColor: theme.current().textfieldBackgroundColor, contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), From bee3ae6e7bb879ee6bebbfad98e17e4a33891dad Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 15:28:33 -0800 Subject: [PATCH 05/47] Fix Debug Layout Issue in AddContact --- lib/views/addcontactview.dart | 290 +++++++++++++++++----------------- lib/views/torstatusview.dart | 23 +-- lib/widgets/textfield.dart | 10 +- 3 files changed, 155 insertions(+), 168 deletions(-) diff --git a/lib/views/addcontactview.dart b/lib/views/addcontactview.dart index 0d920f6c..f8acf20c 100644 --- a/lib/views/addcontactview.dart +++ b/lib/views/addcontactview.dart @@ -52,26 +52,22 @@ class _AddContactViewState extends State { /// We display a different number of tabs depending on the experiment setup bool groupsEnabled = Provider.of(context).isExperimentEnabled(TapirGroupsExperiment); - return Scrollbar( - isAlwaysShown: true, - child: SingleChildScrollView( - clipBehavior: Clip.antiAlias, - child: Consumer(builder: (context, globalErrorHandler, child) { - return DefaultTabController( - length: groupsEnabled ? 2 : 1, - child: Column(children: [ - (groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()), - Expanded( - child: TabBarView( - children: (groupsEnabled - ? [ - addPeerTab(), - addGroupTab(), - ] - : [addPeerTab()]), - )), - ])); - }))); + return Consumer(builder: (context, globalErrorHandler, child) { + return DefaultTabController( + length: groupsEnabled ? 2 : 1, + child: Column(children: [ + (groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()), + Expanded( + child: TabBarView( + children: (groupsEnabled + ? [ + addPeerTab(), + addGroupTab(), + ] + : [addPeerTab()]), + )), + ])); + }); } void _copyOnion() { @@ -109,67 +105,70 @@ class _AddContactViewState extends State { /// The Add Peer Tab allows a peer to add a specific non-group peer to their contact lists /// We also provide a convenient way to copy their onion. Widget addPeerTab() { - return Container( - margin: EdgeInsets.all(30), - padding: EdgeInsets.all(20), - child: Form( - autovalidateMode: AutovalidateMode.always, - key: _formKey, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CwtchLabel(label: AppLocalizations.of(context)!.profileOnionLabel), - SizedBox( - height: 20, - ), - CwtchButtonTextField( - controller: ctrlrOnion, - onPressed: _copyOnion, - readonly: true, - icon: Icon( - CwtchIcons.address_copy_2, - size: 32, - ), - tooltip: AppLocalizations.of(context)!.copyBtn, - ), - SizedBox( - height: 20, - ), - CwtchLabel(label: AppLocalizations.of(context)!.pasteAddressToAddContact), - SizedBox( - height: 20, - ), - CwtchTextField( - controller: ctrlrContact, - validator: (value) { - if (value == "") { - return null; - } - if (globalErrorHandler.invalidImportStringError) { - return AppLocalizations.of(context)!.invalidImportString; - } else if (globalErrorHandler.contactAlreadyExistsError) { - return AppLocalizations.of(context)!.contactAlreadyExists; - } else if (globalErrorHandler.explicitAddContactSuccess) {} - return null; - }, - onChanged: (String importBundle) async { - var profileOnion = Provider.of(context, listen: false).onion; - Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle); + return Scrollbar( + child: SingleChildScrollView( + clipBehavior: Clip.antiAlias, + child: Container( + margin: EdgeInsets.all(30), + padding: EdgeInsets.all(20), + child: Form( + autovalidateMode: AutovalidateMode.always, + key: _formKey, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CwtchLabel(label: AppLocalizations.of(context)!.profileOnionLabel), + SizedBox( + height: 20, + ), + CwtchButtonTextField( + controller: ctrlrOnion, + onPressed: _copyOnion, + readonly: true, + icon: Icon( + CwtchIcons.address_copy_2, + size: 32, + ), + tooltip: AppLocalizations.of(context)!.copyBtn, + ), + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.pasteAddressToAddContact), + SizedBox( + height: 20, + ), + CwtchTextField( + controller: ctrlrContact, + validator: (value) { + if (value == "") { + return null; + } + if (globalErrorHandler.invalidImportStringError) { + return AppLocalizations.of(context)!.invalidImportString; + } else if (globalErrorHandler.contactAlreadyExistsError) { + return AppLocalizations.of(context)!.contactAlreadyExists; + } else if (globalErrorHandler.explicitAddContactSuccess) {} + return null; + }, + onChanged: (String importBundle) async { + var profileOnion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle); - Future.delayed(const Duration(milliseconds: 500), () { - if (globalErrorHandler.importBundleSuccess) { - // TODO: This isn't ideal, but because onChange can be fired during this future check - // and because the context can change after being popped we have this kind of double assertion... - // There is probably a better pattern to handle this... - if (AppLocalizations.of(context) != null) { - final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + importBundle)); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - Navigator.popUntil(context, (route) => route.settings.name == "conversations"); - } - } - }); - }, - hintText: '', - ) - ]))); + Future.delayed(const Duration(milliseconds: 500), () { + if (globalErrorHandler.importBundleSuccess) { + // TODO: This isn't ideal, but because onChange can be fired during this future check + // and because the context can change after being popped we have this kind of double assertion... + // There is probably a better pattern to handle this... + if (AppLocalizations.of(context) != null) { + final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + importBundle)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + Navigator.popUntil(context, (route) => route.settings.name == "conversations"); + } + } + }); + }, + hintText: '', + ) + ]))))); } /// TODO Add Group Pane @@ -179,71 +178,74 @@ class _AddContactViewState extends State { return Text(AppLocalizations.of(context)!.addServerFirst); } - return Container( - margin: EdgeInsets.all(30), - padding: EdgeInsets.all(20), - child: Form( - autovalidateMode: AutovalidateMode.always, - key: _createGroupFormKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CwtchLabel(label: AppLocalizations.of(context)!.server), - SizedBox( - height: 20, - ), - DropdownButton( - onChanged: (String? newServer) { - setState(() { - server = newServer!; - }); - }, - isExpanded: true, // magic property - value: server, - items: Provider.of(context) - .serverList - .servers - .where((serverInfo) => serverInfo.status == "Synced") - .map>((RemoteServerInfoState serverInfo) { - return DropdownMenuItem( - value: serverInfo.onion, - child: Text( - serverInfo.description.isNotEmpty ? serverInfo.description : serverInfo.onion, - overflow: TextOverflow.ellipsis, + return Scrollbar( + child: SingleChildScrollView( + clipBehavior: Clip.antiAlias, + child: Container( + margin: EdgeInsets.all(30), + padding: EdgeInsets.all(20), + child: Form( + autovalidateMode: AutovalidateMode.always, + key: _createGroupFormKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CwtchLabel(label: AppLocalizations.of(context)!.server), + SizedBox( + height: 20, ), - ); - }).toList()), - SizedBox( - height: 20, - ), - CwtchLabel(label: AppLocalizations.of(context)!.groupName), - SizedBox( - height: 20, - ), - CwtchTextField( - controller: ctrlrGroupName, - hintText: AppLocalizations.of(context)!.groupNameLabel, - onChanged: (newValue) {}, - validator: (value) {}, - ), - SizedBox( - height: 20, - ), - ElevatedButton( - onPressed: () { - var profileOnion = Provider.of(context, listen: false).onion; - Provider.of(context, listen: false).cwtch.CreateGroup(profileOnion, server, ctrlrGroupName.text); - Future.delayed(const Duration(milliseconds: 500), () { - final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + " " + ctrlrGroupName.text)); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - Navigator.pop(context); - }); - }, - child: Text(AppLocalizations.of(context)!.createGroupBtn), - ), - ], - ))); + DropdownButton( + onChanged: (String? newServer) { + setState(() { + server = newServer!; + }); + }, + isExpanded: true, // magic property + value: server, + items: Provider.of(context) + .serverList + .servers + .where((serverInfo) => serverInfo.status == "Synced") + .map>((RemoteServerInfoState serverInfo) { + return DropdownMenuItem( + value: serverInfo.onion, + child: Text( + serverInfo.description.isNotEmpty ? serverInfo.description : serverInfo.onion, + overflow: TextOverflow.ellipsis, + ), + ); + }).toList()), + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.groupName), + SizedBox( + height: 20, + ), + CwtchTextField( + controller: ctrlrGroupName, + hintText: AppLocalizations.of(context)!.groupNameLabel, + onChanged: (newValue) {}, + validator: (value) {}, + ), + SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: () { + var profileOnion = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.CreateGroup(profileOnion, server, ctrlrGroupName.text); + Future.delayed(const Duration(milliseconds: 500), () { + final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + " " + ctrlrGroupName.text)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + Navigator.pop(context); + }); + }, + child: Text(AppLocalizations.of(context)!.createGroupBtn), + ), + ], + ))))); } /// TODO Manage Servers Tab diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 1a63899c..982bfbaa 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -106,14 +106,10 @@ class _TorStatusView extends State { if (port > 0 && port < 65536) { return null; } else { - return AppLocalizations.of( - context)! - .torSettingsErrorSettingPort; + return AppLocalizations.of(context)!.torSettingsErrorSettingPort; } - }catch (e) { - return AppLocalizations.of( - context)! - .torSettingsErrorSettingPort; + } catch (e) { + return AppLocalizations.of(context)!.torSettingsErrorSettingPort; } }, onChanged: (String socksPort) { @@ -141,22 +137,17 @@ class _TorStatusView extends State { if (port > 0 && port < 65536) { return null; } else { - return AppLocalizations.of( - context)! - .torSettingsErrorSettingPort; + return AppLocalizations.of(context)!.torSettingsErrorSettingPort; } - }catch (e) { - return AppLocalizations.of( - context)! - .torSettingsErrorSettingPort; + } catch (e) { + return AppLocalizations.of(context)!.torSettingsErrorSettingPort; } }, onChanged: (String controlPort) { try { var port = int.parse(controlPort); if (port > 0 && port < 65536) { - settings.controlPort = - int.parse(controlPort); + settings.controlPort = int.parse(controlPort); saveSettings(context); } } catch (e) {} diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index 9f5ff620..b62c84c8 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -51,9 +51,7 @@ class _CwtchTextFieldState extends State { : widget.number ? TextInputType.number : TextInputType.text, - inputFormatters: widget.number ? [ - FilteringTextInputFormatter.digitsOnly - ] : null, + inputFormatters: widget.number ? [FilteringTextInputFormatter.digitsOnly] : null, maxLines: widget.multiLine ? null : 1, scrollController: _scrollController, enableIMEPersonalizedLearning: false, @@ -66,11 +64,7 @@ class _CwtchTextFieldState extends State { focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)), focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), - errorStyle: TextStyle( - color: theme.current().textfieldErrorColor, - fontWeight: FontWeight.bold, - overflow: TextOverflow.visible - ), + errorStyle: TextStyle(color: theme.current().textfieldErrorColor, fontWeight: FontWeight.bold, overflow: TextOverflow.visible), fillColor: theme.current().textfieldBackgroundColor, contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0))), From 9d3d5b06e5712c20e2cebde05d5bc00480c59d1a Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 15:54:07 -0800 Subject: [PATCH 06/47] Upgrade Android Dependencies. Remove references to jCenter --- android/app/build.gradle | 3 ++- android/build.gradle | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ac09ca3..4899ea9e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -89,7 +89,8 @@ dependencies { implementation project(':cwtch') implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2" - implementation "com.airbnb.android:lottie:3.5.0" + implementation "com.airbnb.android:lottie:4.2.1" + implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0" implementation "com.android.support.constraint:constraint-layout:2.0.4" // WorkManager diff --git a/android/build.gradle b/android/build.gradle index 5a12dade..0827984d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,8 @@ buildscript { ext.kotlin_version = '1.3.50' repositories { google() - jcenter() + // jCenter() no longer exists... https://blog.gradle.org/jcenter-shutdown + mavenCentral() } dependencies { @@ -15,7 +16,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } From a3e2da8469ffe9f8c70424db55551009bc8d2dec Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 12 Jan 2022 16:05:58 -0800 Subject: [PATCH 07/47] update text field golden --- test/textfield_form_alpha.png | Bin 8123 -> 8237 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/textfield_form_alpha.png b/test/textfield_form_alpha.png index 531bb36ab74c681fd472f67e044e5a16a8910eb2..1eff8ae81debab26d982a6df3bdc93395a36ec09 100644 GIT binary patch literal 8237 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 literal 8123 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 From daa89bf6e72ecc975632f7da8772fd9e96ab8079 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 13 Jan 2022 15:21:09 -0800 Subject: [PATCH 08/47] Add Circuit Info To Peer Settings --- lib/cwtch/cwtchNotifier.dart | 4 ++++ lib/model.dart | 7 ++++++ lib/views/peersettingsview.dart | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index ab03a8cb..80e0643d 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -351,6 +351,10 @@ class CwtchNotifier { case "DoneStorageMigration": appState.SetModalState(ModalState.none); break; + case "ACNInfo": + var handle = data["Handle"]; + profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(handle)?.acnCircuit = data["Data"]; + break; default: EnvironmentConfig.debugLog("unhandled event: $type"); } diff --git a/lib/model.dart b/lib/model.dart index 40a98110..035b73d2 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -565,6 +565,8 @@ class ContactInfoState extends ChangeNotifier { String? _server; late bool _archived; + String? _acnCircuit; + ContactInfoState(this.profileOnion, this.identifier, this.onion, {nickname = "", isGroup = false, @@ -598,6 +600,11 @@ class ContactInfoState extends ChangeNotifier { String get savePeerHistory => this._savePeerHistory; + String? get acnCircuit => this._acnCircuit; + set acnCircuit(String? acnCircuit) { + this._acnCircuit = acnCircuit; + } + // Indicated whether the conversation is archived, in which case it will // be moved to the very bottom of the active conversations list until // new messages appear diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 2c11bcb3..1456d176 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:flutter/services.dart'; import 'package:cwtch/model.dart'; @@ -47,6 +48,33 @@ class _PeerSettingsViewState extends State { Widget _buildSettingsList() { return Consumer(builder: (context, settings, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { + String? acnCircuit = Provider.of(context).acnCircuit; + + Widget path = Text(Provider.of(context, listen: false).status); + + if (acnCircuit != null) { + var hops = acnCircuit.split(","); + if (hops.length == 3) { + List paths = hops.map((String countryCodeAndIp) { + var parts = countryCodeAndIp.split(":"); + var country = parts[0]; + var ip = parts[1]; + return RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: "$country", + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, fontFamily: "monospace"), + children: [TextSpan(text: " ($ip)", style: TextStyle(fontSize: 8, fontWeight: FontWeight.normal))])); + }).toList(growable: true); + + paths.add(RichText(text: TextSpan(text: "Tor Network", style: TextStyle(fontWeight: FontWeight.normal, fontSize: 8, fontFamily: "monospace")))); + + path = Column( + children: paths, + ); + } + } + return Scrollbar( isAlwaysShown: true, child: SingleChildScrollView( @@ -104,6 +132,16 @@ class _PeerSettingsViewState extends State { SizedBox( height: 20, ), + ListTile( + leading: Icon(CwtchIcons.onion_on, color: settings.current().mainTextColor), + isThreeLine: true, + title: Text("ACN Circuit Info"), + subtitle: Text("In depth information about the path that the anonymous communication network is using to connect to this conversation"), + trailing: path, + ), + SizedBox( + height: 20, + ), SwitchListTile( title: Text(AppLocalizations.of(context)!.blockBtn, style: TextStyle(color: settings.current().mainTextColor)), value: Provider.of(context).isBlocked, From ed671d32bcb68696c76fed016d61d4750c1bcb30 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 14 Jan 2022 14:19:35 -0800 Subject: [PATCH 09/47] Padding / Margin Changes + Tor Circuit Info --- lib/l10n/intl_de.arb | 4 +- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_es.arb | 4 +- lib/l10n/intl_fr.arb | 4 +- lib/l10n/intl_it.arb | 4 +- lib/l10n/intl_pl.arb | 4 +- lib/l10n/intl_pt.arb | 4 +- lib/l10n/intl_ru.arb | 4 +- lib/themes/opaque.dart | 2 +- lib/views/contactsview.dart | 3 ++ lib/views/globalsettingsview.dart | 36 ++++++++------- lib/views/messageview.dart | 5 +++ lib/views/profilemgrview.dart | 9 ++-- lib/widgets/buttontextfield.dart | 9 ++-- lib/widgets/contactrow.dart | 3 ++ lib/widgets/folderpicker.dart | 74 ++++++++++++++++--------------- lib/widgets/messagerow.dart | 1 + lib/widgets/passwordfield.dart | 10 +++-- lib/widgets/profilerow.dart | 1 + lib/widgets/serverrow.dart | 2 + lib/widgets/textfield.dart | 10 ++--- 21 files changed, 120 insertions(+), 77 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 05071e96..e35abc53 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,8 @@ { "@@locale": "de", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ff0be443..162e9907 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,8 @@ { "@@locale": "en", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "settingTheme": "Use Light Themes", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 9c928936..135446cb 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,8 @@ { "@@locale": "es", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 3e90f9fd..60a7495c 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,8 @@ { "@@locale": "fr", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index b392a0ff..00354ba7 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,8 @@ { "@@locale": "it", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 80ebcac5..2a9271cc 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,8 @@ { "@@locale": "pl", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 31ce6b76..5f4ec025 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,8 @@ { "@@locale": "pt", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.", "torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index dc524e28..e43bedaa 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,8 @@ { "@@locale": "ru", - "@@last_modified": "2022-01-12T23:54:51+01:00", + "@@last_modified": "2022-01-14T22:08:34+01:00", + "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", + "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", "msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.", "btnSendFile": "Отправить файл", diff --git a/lib/themes/opaque.dart b/lib/themes/opaque.dart index 003b1095..ea219674 100644 --- a/lib/themes/opaque.dart +++ b/lib/themes/opaque.dart @@ -176,7 +176,7 @@ ThemeData mkThemeData(Settings opaque) { splashFactory: InkRipple.splashFactory, padding: MaterialStateProperty.all(EdgeInsets.all(20)), shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), + borderRadius: BorderRadius.circular(6.0), )), ), ), diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index cfb29b01..09cc7d5b 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -109,6 +109,7 @@ class _ContactsViewState extends State { actions.add(IconButton( icon: Icon(CwtchIcons.address_copy_2), tooltip: AppLocalizations.of(context)!.copyAddress, + splashRadius: Material.defaultSplashRadius / 2, onPressed: () { Clipboard.setData(new ClipboardData(text: Provider.of(context, listen: false).onion)); })); @@ -118,6 +119,7 @@ class _ContactsViewState extends State { actions.add(IconButton( icon: Icon(CwtchIcons.dns_24px), tooltip: AppLocalizations.of(context)!.manageKnownServersButton, + splashRadius: Material.defaultSplashRadius / 2, onPressed: () { _pushServers(); })); @@ -127,6 +129,7 @@ class _ContactsViewState extends State { actions.add(IconButton( // need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset icon: Icon(showSearchBar || Provider.of(context).isFiltered ? Icons.search_off : Icons.search), + splashRadius: Material.defaultSplashRadius / 2, onPressed: () { Provider.of(context, listen: false).filter = ""; setState(() { diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index c2d9880c..78e3a829 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -280,32 +280,36 @@ class _GlobalSettingsViewState extends State { child: CwtchFolderPicker( label: AppLocalizations.of(context)!.settingDownloadFolder, initialValue: settings.downloadPath, + description: AppLocalizations.of(context)!.fileSharingSettingsDownloadFolderDescription, + tooltip: AppLocalizations.of(context)!.fileSharingSettingsDownloadFolderTooltip, onSave: (newVal) { settings.downloadPath = newVal; saveSettings(context); }, ), ), - SwitchListTile( - title: Text(AppLocalizations.of(context)!.enableExperimentClickableLinks, style: TextStyle(color: settings.current().mainTextColor)), - subtitle: Text(AppLocalizations.of(context)!.experimentClickableLinksDescription), - value: settings.isExperimentEnabled(ClickableLinksExperiment), - onChanged: (bool value) { - if (value) { - settings.enableExperiment(ClickableLinksExperiment); - } else { - settings.disableExperiment(ClickableLinksExperiment); - } - saveSettings(context); - }, - activeTrackColor: settings.theme.defaultButtonActiveColor, - inactiveTrackColor: settings.theme.defaultButtonDisabledColor, - secondary: Icon(Icons.link, color: settings.current().mainTextColor), - ), ]), ), ], )), + Visibility( + visible: settings.experimentsEnabled, + child: SwitchListTile( + title: Text(AppLocalizations.of(context)!.enableExperimentClickableLinks, style: TextStyle(color: settings.current().mainTextColor)), + subtitle: Text(AppLocalizations.of(context)!.experimentClickableLinksDescription), + value: settings.isExperimentEnabled(ClickableLinksExperiment), + onChanged: (bool value) { + if (value) { + settings.enableExperiment(ClickableLinksExperiment); + } else { + settings.disableExperiment(ClickableLinksExperiment); + } + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonActiveColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(Icons.link, color: settings.current().mainTextColor), + )), AboutListTile( icon: appIcon, applicationIcon: Padding(padding: EdgeInsets.all(5), child: Icon(CwtchIcons.cwtch_knott)), diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index c8d775a5..5d21ab8e 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -85,6 +85,7 @@ class _MessageViewState extends State { if (Provider.of(context).isOnline()) { if (showFileSharing) { appBarButtons.add(IconButton( + splashRadius: Material.defaultSplashRadius / 2, icon: Icon(Icons.attach_file, size: 24), tooltip: AppLocalizations.of(context)!.tooltipSendFile, onPressed: () { @@ -93,6 +94,7 @@ class _MessageViewState extends State { )); } appBarButtons.add(IconButton( + splashRadius: Material.defaultSplashRadius / 2, icon: Icon(CwtchIcons.send_invite, size: 24), tooltip: AppLocalizations.of(context)!.sendInvite, onPressed: () { @@ -100,6 +102,7 @@ class _MessageViewState extends State { })); } appBarButtons.add(IconButton( + splashRadius: Material.defaultSplashRadius / 2, icon: Provider.of(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px), tooltip: AppLocalizations.of(context)!.conversationSettings, onPressed: _pushContactSettings)); @@ -263,6 +266,7 @@ class _MessageViewState extends State { focusedBorder: InputBorder.none, enabled: true, suffixIcon: ElevatedButton( + style: ElevatedButton.styleFrom(padding: EdgeInsets.all(0.0), shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0))), child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of(context).theme.defaultButtonTextColor), onPressed: isOffline ? null : _sendMessage, ))), @@ -291,6 +295,7 @@ class _MessageViewState extends State { alignment: Alignment.topRight, child: IconButton( icon: Icon(Icons.highlight_remove), + splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage, onPressed: () { Provider.of(context, listen: false).selectedIndex = null; diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index da72472c..97a7c21b 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -83,6 +83,7 @@ class _ProfileMgrViewState extends State { actions.add(IconButton( icon: TorIcon(), onPressed: _pushTorStatus, + splashRadius: Material.defaultSplashRadius / 2, tooltip: Provider.of(context).progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : (Provider.of(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor), @@ -93,6 +94,7 @@ class _ProfileMgrViewState extends State { // Unlock Profiles actions.add(IconButton( icon: Icon(CwtchIcons.lock_open_24px), + splashRadius: Material.defaultSplashRadius / 2, color: Provider.of(context).profiles.isEmpty ? Provider.of(context).theme.defaultButtonColor : Provider.of(context).theme.mainTextColor, tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles, onPressed: _modalUnlockProfiles, @@ -100,14 +102,15 @@ class _ProfileMgrViewState extends State { // Servers if (Provider.of(context).isExperimentEnabled(ServerManagementExperiment) && !Platform.isAndroid && !Platform.isIOS) { - actions.add(IconButton(icon: Icon(CwtchIcons.dns_black_24dp), tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers)); + actions.add( + IconButton(icon: Icon(CwtchIcons.dns_black_24dp), splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers)); } // Global Settings - actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings)); + actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, splashRadius: Material.defaultSplashRadius / 2, onPressed: _pushGlobalSettings)); // shutdown cwtch - actions.add(IconButton(icon: Icon(Icons.close), tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip, onPressed: _modalShutdown)); + actions.add(IconButton(icon: Icon(Icons.close), tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip, splashRadius: Material.defaultSplashRadius / 2, onPressed: _modalShutdown)); return actions; } diff --git a/lib/widgets/buttontextfield.dart b/lib/widgets/buttontextfield.dart index dddb7840..389ef4ae 100644 --- a/lib/widgets/buttontextfield.dart +++ b/lib/widgets/buttontextfield.dart @@ -45,6 +45,7 @@ class _CwtchButtonTextFieldState extends State { suffixIcon: IconButton( onPressed: widget.onPressed, icon: widget.icon, + splashRadius: Material.defaultSplashRadius / 2, padding: EdgeInsets.fromLTRB(0.0, 4.0, 2.0, 2.0), tooltip: widget.tooltip, enableFeedback: true, @@ -56,15 +57,15 @@ class _CwtchButtonTextFieldState extends State { floatingLabelBehavior: FloatingLabelBehavior.never, filled: true, fillColor: theme.current().textfieldBackgroundColor, - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)), - focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), - errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0)), + focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 1.0)), + errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 1.0)), errorStyle: TextStyle( color: theme.current().textfieldErrorColor, fontWeight: FontWeight.bold, ), contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0))), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0))), ); }); } diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index cdba90de..ee1ba242 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -78,6 +78,7 @@ class _ContactRowState extends State { ? Wrap(direction: Axis.vertical, children: [ IconButton( padding: EdgeInsets.zero, + splashRadius: Material.defaultSplashRadius / 2, iconSize: 16, icon: Icon( Icons.favorite, @@ -88,6 +89,7 @@ class _ContactRowState extends State { ), IconButton( padding: EdgeInsets.zero, + splashRadius: Material.defaultSplashRadius / 2, iconSize: 16, icon: Icon(Icons.delete, color: Provider.of(context).theme.mainTextColor), tooltip: AppLocalizations.of(context)!.tooltipRejectContactRequest, @@ -97,6 +99,7 @@ class _ContactRowState extends State { : (contact.isBlocked != null && contact.isBlocked ? IconButton( padding: EdgeInsets.zero, + splashRadius: Material.defaultSplashRadius / 2, iconSize: 16, icon: Icon(Icons.block, color: Provider.of(context).theme.mainTextColor), onPressed: () {}, diff --git a/lib/widgets/folderpicker.dart b/lib/widgets/folderpicker.dart index 7e9f9bfd..b56e5432 100644 --- a/lib/widgets/folderpicker.dart +++ b/lib/widgets/folderpicker.dart @@ -1,16 +1,21 @@ +import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'dart:io'; import 'package:file_picker_desktop/file_picker_desktop.dart'; +import 'package:provider/provider.dart'; +import '../settings.dart'; import 'buttontextfield.dart'; import 'cwtchlabel.dart'; class CwtchFolderPicker extends StatefulWidget { final String label; final String initialValue; + final String tooltip; + final String description; final Function(String)? onSave; - const CwtchFolderPicker({Key? key, this.label = "", this.initialValue = "", this.onSave}) : super(key: key); + const CwtchFolderPicker({Key? key, this.label = "", this.tooltip = "", this.initialValue = "", this.onSave, this.description = ""}) : super(key: key); @override _CwtchFolderPickerState createState() => _CwtchFolderPickerState(); @@ -27,41 +32,38 @@ class _CwtchFolderPickerState extends State { @override Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.all(10), - padding: EdgeInsets.all(2), - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - CwtchLabel(label: widget.label), - SizedBox( - height: 20, - ), - CwtchButtonTextField( - controller: ctrlrVal, - readonly: Platform.isAndroid, - onPressed: () async { - if (Platform.isAndroid) { - return; - } - - try { - var selectedDirectory = await getDirectoryPath(); - if (selectedDirectory != null) { - //File directory = File(selectedDirectory); - selectedDirectory += "/"; - ctrlrVal.text = selectedDirectory; - if (widget.onSave != null) { - widget.onSave!(selectedDirectory); - } - } else { - // User canceled the picker + return ListTile( + leading: Icon(Icons.file_download, color: Provider.of(context).theme.messageFromMeTextColor, size: 16), + title: Text(widget.label), + subtitle: Text(widget.description), + trailing: Container( + width: 200, + child: CwtchButtonTextField( + controller: ctrlrVal, + readonly: Platform.isAndroid, + onPressed: () async { + if (Platform.isAndroid) { + return; } - } catch (e) { - print(e); - } - }, - icon: Icon(Icons.folder), - tooltip: "Browse", //todo: l18n - ) - ])); + + try { + var selectedDirectory = await getDirectoryPath(); + if (selectedDirectory != null) { + //File directory = File(selectedDirectory); + selectedDirectory += "/"; + ctrlrVal.text = selectedDirectory; + if (widget.onSave != null) { + widget.onSave!(selectedDirectory); + } + } else { + // User canceled the picker + } + } catch (e) { + print(e); + } + }, + icon: Icon(Icons.folder), + tooltip: widget.tooltip, //todo: l18n + ))); } } diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 14514133..2cc51647 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -85,6 +85,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi maintainInteractivity: false, child: IconButton( tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, + splashRadius: Material.defaultSplashRadius / 2, onPressed: () { Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageID; }, diff --git a/lib/widgets/passwordfield.dart b/lib/widgets/passwordfield.dart index e8cedd9d..ede308ad 100644 --- a/lib/widgets/passwordfield.dart +++ b/lib/widgets/passwordfield.dart @@ -37,6 +37,7 @@ class _CwtchTextFieldState extends State { controller: widget.controller, validator: widget.validator, obscureText: obscureText, + obscuringCharacter: '*', enableIMEPersonalizedLearning: false, autofillHints: widget.autoFillHints, autovalidateMode: AutovalidateMode.always, @@ -57,17 +58,18 @@ class _CwtchTextFieldState extends State { highlightColor: theme.current().defaultButtonColor, focusColor: theme.current().defaultButtonActiveColor, splashColor: theme.current().defaultButtonActiveColor, + splashRadius: Material.defaultSplashRadius / 2, ), errorStyle: TextStyle( color: theme.current().textfieldErrorColor, fontWeight: FontWeight.bold, ), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)), - focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), - errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0)), + focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 1.0)), + errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 1.0)), filled: true, fillColor: theme.current().textfieldBackgroundColor, - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0)), ), ); }); diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index 3c4c1a55..7fccb90f 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -59,6 +59,7 @@ class _ProfileRowState extends State { )), IconButton( enableFeedback: true, + splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname, icon: Icon(Icons.create, color: Provider.of(context).current().mainTextColor), onPressed: () { diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index 2c245b51..cb5790df 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -57,6 +57,7 @@ class _ServerRowState extends State { // Copy server button IconButton( enableFeedback: true, + splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.copyServerKeys, icon: Icon(CwtchIcons.address_copy_2, color: Provider.of(context).current().mainTextColor), onPressed: () { @@ -67,6 +68,7 @@ class _ServerRowState extends State { // Edit button IconButton( enableFeedback: true, + splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.editServerTitle, icon: Icon(Icons.create, color: Provider.of(context).current().mainTextColor), onPressed: () { diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index b62c84c8..d7d3f5fd 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -61,13 +61,13 @@ class _CwtchTextFieldState extends State { hintText: widget.hintText, floatingLabelBehavior: FloatingLabelBehavior.never, filled: true, - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0)), - focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), - errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 3.0)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0)), + focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 1.0)), + errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor, width: 1.0)), errorStyle: TextStyle(color: theme.current().textfieldErrorColor, fontWeight: FontWeight.bold, overflow: TextOverflow.visible), fillColor: theme.current().textfieldBackgroundColor, - contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 3.0))), + contentPadding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 5.0), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(6.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor, width: 1.0))), ); }); } From ae6f0dd456ad3ae84959a3bb8eee180ce73d9bea Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 17 Jan 2022 12:24:48 -0800 Subject: [PATCH 10/47] Update Translations + notifylisteners --- lib/l10n/intl_de.arb | 5 ++++- lib/l10n/intl_en.arb | 5 ++++- lib/l10n/intl_es.arb | 5 ++++- lib/l10n/intl_fr.arb | 5 ++++- lib/l10n/intl_it.arb | 5 ++++- lib/l10n/intl_pl.arb | 5 ++++- lib/l10n/intl_pt.arb | 5 ++++- lib/l10n/intl_ru.arb | 5 ++++- lib/model.dart | 1 + lib/views/peersettingsview.dart | 6 +++--- lib/widgets/folderpicker.dart | 2 +- 11 files changed, 37 insertions(+), 12 deletions(-) diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index e35abc53..1d4bcc7a 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,9 @@ { "@@locale": "de", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 162e9907..b38690f5 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,9 @@ { "@@locale": "en", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 135446cb..403fc323 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,9 @@ { "@@locale": "es", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 60a7495c..dc576196 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,9 @@ { "@@locale": "fr", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 00354ba7..43902cb0 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,9 @@ { "@@locale": "it", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 2a9271cc..f66ea23e 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,9 @@ { "@@locale": "pl", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 5f4ec025..95ac2390 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,9 @@ { "@@locale": "pt", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index e43bedaa..0a1f3102 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,9 @@ { "@@locale": "ru", - "@@last_modified": "2022-01-14T22:08:34+01:00", + "@@last_modified": "2022-01-17T21:20:54+01:00", + "labelTorNetwork": "Tor Network", + "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", + "labelACNCircuitInfo": "ACN Circuit Info", "fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.", "fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.", "torSettingsErrorSettingPort": "Port Number must be between 1 and 65535", diff --git a/lib/model.dart b/lib/model.dart index 035b73d2..923f0f46 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -603,6 +603,7 @@ class ContactInfoState extends ChangeNotifier { String? get acnCircuit => this._acnCircuit; set acnCircuit(String? acnCircuit) { this._acnCircuit = acnCircuit; + notifyListeners() } // Indicated whether the conversation is archived, in which case it will diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 1456d176..96f15567 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -67,7 +67,7 @@ class _PeerSettingsViewState extends State { children: [TextSpan(text: " ($ip)", style: TextStyle(fontSize: 8, fontWeight: FontWeight.normal))])); }).toList(growable: true); - paths.add(RichText(text: TextSpan(text: "Tor Network", style: TextStyle(fontWeight: FontWeight.normal, fontSize: 8, fontFamily: "monospace")))); + paths.add(RichText(text: TextSpan(text: AppLocalizations.of(context)!.labelTorNetwork, style: TextStyle(fontWeight: FontWeight.normal, fontSize: 8, fontFamily: "monospace")))); path = Column( children: paths, @@ -135,8 +135,8 @@ class _PeerSettingsViewState extends State { ListTile( leading: Icon(CwtchIcons.onion_on, color: settings.current().mainTextColor), isThreeLine: true, - title: Text("ACN Circuit Info"), - subtitle: Text("In depth information about the path that the anonymous communication network is using to connect to this conversation"), + title: Text(AppLocalizations.of(context)!.labelACNCircuitInfo), + subtitle: Text(AppLocalizations.of(context)!.descriptionACNCircuitInfo), trailing: path, ), SizedBox( diff --git a/lib/widgets/folderpicker.dart b/lib/widgets/folderpicker.dart index b56e5432..9423aec4 100644 --- a/lib/widgets/folderpicker.dart +++ b/lib/widgets/folderpicker.dart @@ -63,7 +63,7 @@ class _CwtchFolderPickerState extends State { } }, icon: Icon(Icons.folder), - tooltip: widget.tooltip, //todo: l18n + tooltip: widget.tooltip, ))); } } From d6ecf872550ba7b68774c9c9971cfd4ef89860d5 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 17 Jan 2022 12:29:13 -0800 Subject: [PATCH 11/47] PR Fixups --- lib/model.dart | 2 +- lib/views/peersettingsview.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model.dart b/lib/model.dart index 923f0f46..00ebba2e 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -603,7 +603,7 @@ class ContactInfoState extends ChangeNotifier { String? get acnCircuit => this._acnCircuit; set acnCircuit(String? acnCircuit) { this._acnCircuit = acnCircuit; - notifyListeners() + notifyListeners(); } // Indicated whether the conversation is archived, in which case it will diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 96f15567..6606312d 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -62,7 +62,7 @@ class _PeerSettingsViewState extends State { return RichText( textAlign: TextAlign.left, text: TextSpan( - text: "$country", + text: country, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 10, fontFamily: "monospace"), children: [TextSpan(text: " ($ip)", style: TextStyle(fontSize: 8, fontWeight: FontWeight.normal))])); }).toList(growable: true); From 5494cb5de0389c74d3025bf0ced2b3c7f91692e5 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 17 Jan 2022 14:52:15 -0800 Subject: [PATCH 12/47] Upgrade LibCwtch-Go --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- lib/cwtch/cwtchNotifier.dart | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 270b0601..c1538941 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-12-17-26-v1.5.4-3-gaf47036 \ No newline at end of file +2022-01-17-17-19-v1.5.4-5-g4cf95d6 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 68121b4a..ed75fff0 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-12-22-26-v1.5.4-3-gaf47036 \ No newline at end of file +2022-01-17-22-19-v1.5.4-5-g4cf95d6 \ No newline at end of file diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 80e0643d..f400f910 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -352,8 +352,11 @@ class CwtchNotifier { appState.SetModalState(ModalState.none); break; case "ACNInfo": + var key = data["Key"]; var handle = data["Handle"]; - profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(handle)?.acnCircuit = data["Data"]; + if (key == "circuit") { + profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(handle)?.acnCircuit = data["Data"]; + } break; default: EnvironmentConfig.debugLog("unhandled event: $type"); From 7257e2bca0f5758e66b8e51ec3a0fb64b5f8d472 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 17 Jan 2022 14:55:37 -0800 Subject: [PATCH 13/47] Update Goldens --- test/textfield_basic.png | Bin 6275 -> 5219 bytes test/textfield_form_42.png | Bin 6263 -> 5206 bytes test/textfield_form_alpha.png | Bin 8237 -> 6277 bytes test/textfield_form_final.png | Bin 8060 -> 6024 bytes test/textfield_form_init.png | Bin 6235 -> 5180 bytes test/textfield_init.png | Bin 6083 -> 5032 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/test/textfield_basic.png b/test/textfield_basic.png index b17efd0799d2463672d6a97af1443a43aa3c5523..7d4b7df27ad17bbd4bb37294c4522096436483e3 100644 GIT binary patch literal 5219 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 E10Y_A5&!@I literal 6275 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 literal 6263 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 diff --git a/test/textfield_form_alpha.png b/test/textfield_form_alpha.png index 1eff8ae81debab26d982a6df3bdc93395a36ec09..c9dcbee7763ab08b08c264aafc5d4e64dd1e35c8 100644 GIT binary patch literal 6277 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 literal 8237 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 diff --git a/test/textfield_form_final.png b/test/textfield_form_final.png index d9087d17fb9ac4131f7afb7e9382efe4da927b90..03c23e2c37cceedb2dd364fb4cf8116f0793bbb1 100644 GIT binary patch literal 6024 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 TAAE}O9uNp=Xkk!{aC-7zyE|u1 literal 8060 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 diff --git a/test/textfield_form_init.png b/test/textfield_form_init.png index 5c23672c2eb79287ff27c7a9c9916a15941c9894..facd8e5e8974aca13051a477490a4da0efa6cdb7 100644 GIT binary patch literal 5180 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 VPI3JjvZrAt>_wbWvEj}C{1+07y4e5# diff --git a/test/textfield_init.png b/test/textfield_init.png index 94ce9bc4add9091e05fae90eb75355997e568a6a..fe7169e42a6f3a8ebffd498efa5de42037b9a1d4 100644 GIT binary patch literal 5032 zcmeHLX+Trgwmvw%5_C^A$< zQ5mA77?4R@1;SVv4IzLKpp20z2!Z786MXO8_v8J0KX3kIo$su@_gddxYwfj9nuDG7 zo`1;y1A?GEcpHmz5VTbVf+W-tI{;#QqR$BYNra!X#zAH63X|YrTR0BygaE%-1R)iI zWX|C&PCH%Ao#nWa2`i^3<~{PJrl#`eCY3zAj^~%;?LM&W#96XKrV6DoEHc@}KlGtx zan=Ggi;q$dD*tBh2oi%KZF}qdi}bv-dg!gIHhNyN<$^>j)M>YOUV(8pw9E7I($x;K zbxb*<`C#Ia2SvZyHR?Drun}wB6N}nK&pXCwXHB~=iWX^I2nee9%v@epj=osE;rb0e zrq}yk_x;+zzWCL2{dk|=QO~s;x)SiAYrZe9{XNga`HP?@Ia&EQHQ6P@cW;YjsA%V% zJA*4(v$JM-%g%Q-B(^{fs%X-485Ic9RmDBkhJiBx*b4)PaZk}O;2n`lX^a;zM3$

l{DK43u}`!e`VBqjueZqS>+K}k3Id0(#La{kv`7iL z_VIlJS6Y3(+1j&$=04GS$D&)&ZB36_qofaQ$wjx_>V&*1j7LsyTG2eyIE(9u7fvq+ zL(s93DP$P^%RyEBkp9SwwwaFoW*! z5>fS2EQUFfCbL>+t=#s0qQE}%6R!6LdZL;`wK8DbDNiWsQlrM2GH;CL`%&!&LI?@H zqrYAVKUqAxoa<(k_3%UMxj@6xajy&gYF+Nmc8@;TsHUGf#y`Cix@+CSDXkHeW_LCZ zmZVTIt5NmK7K1Av8dC;EQ_~^b8wKmET(gy|*y5_3>EbTE;6{SW`iTY_?(S-!gb1+n`S&Y6o|bMJvDvK85o7s~6grcb50N ze_vd6xB3FFE$?`g@7UV(!?QCxQ@=paQxa#~YtXNZtH^2be`414k}R*x6xuee8g_?8(~_=y zy1K^ibYLWPX35AU;Dm47YT1>TzH&NSZf4DaYD|s2mbsWAucBJ!@ z@E>402qaz9qis#kW;6{vzT=OM=1ogs@CU9Z56~lx+D4q$3r|5%N-G-o1gX3g&Ha0y zrOxr$3b*OHtXiSFokrewzEOf<+a_;3Hk;e8HF4;$dHkm}W>`)+f&c*ly7J>pEk`ad2;n1}%`rt>n7OLx-cO0yd^B(ug=uncGStj>liebkK ziG^D;G?7hl)z#77;+WpKaQ1$Xj0!)K6n>7~ol1Gs<#&o$vJ0lxThNw7S2g<@EXFg>-}J{xF2+I&19JVD+grzSyOKKM`sCOvA66zOnp?fcgjyS^KU#K z^|-aJE#!@Cg*+vL7#9{>({v^!H!9|<+tAn@7v}1YE5|K#UHg11CHyrbOUHG>SNrB` z|5MjXP7K|8IIp-UH~puA@r>FeCn+7In%Y40c1TPANE*dO%&`3N8^ypy<=E*5mu?zkU9?vesB4m+I_apsO=8rfPDYEh-Y3ff0F^4>glnqO%h)7hR~bX>hmNgLviSm7tI$wWJrV37|xqT za}C}#zG>z}jd|CLR!!EK1qVClz{}MWPQ=WgCRB!O{Y6pj>2%FX2^)_XH{B(#=FQ&S7K=f9s&`(mJ8~)0qR4hcKwy=jqTt7kU^pre@KKN2P33*J*dKv4hlDZ8!afG0Rwnzl7A3_(d!IM>>anHrH8KVndivbMoIUd$5E;NFH z@Y5uxB;dLyF_prCPsrB&NNrb`*eX?=k2O%$%C=||2BgE$Hk>sgjY34?hY&qo}E zc}z^Dl*0;@b|eK;aF#+lYkne`eEw6}_Ut~oQoq9i``jie6JbafZ*~kYRvL!X4oFnU z5Jf=H-8Yeex*fR&w~5zdmvsu-S`y~7Cszb?rQW};CnvA}ov_;R@3~`I`0^fb-cv&% zch|MKZ#-;6d?%baH+$TI&M!o&Gdv}rH$OPbXvYEjA>wJ0ExhfCY>*_X<`CqGKoi71 z@5YmC?SN-*5Ee}Ym;|y7iEIx82Q8YeiUB-{EY{d&(c}jMwl<{QVvT*Nluxj)clRR0 zV&G+-*pLQt;AM`=p!DI6U>K1~VTgs)R0vxt@mPlO7dkxU^Jk8vnc9VOy*Iq1#Zx|is(}ofU!6GTR;iULw&(o$6vV%`BG}lG zcgA()wHrkYBP+1ho83J}aXGRY>hYfblW~men@(KFMzGe$^=U-VqVY=6n?`k`aC86F zmxb)#bYY~@y6KP3bD`CnuAx#?xiPz{`fnp>3<)BGIyx^Jg>MfdbyvUOT zhs4vTqx(=atRP4@Jj0qG@>fo8!Y@Xp#V|IGAJlt&gw5y=srq(KRs|3qdj2NmbS$PS zI1$qeyZqAOJz|x=sLAb9j{njS2ydJ2xyPGOl(46sX-5km6{nc4BWO0>S?`5aBJ^7j zh6k0?VB)K^ZXgU5zRG8PNHw78gLi2BJ2|*xv=MK7-Xk)_^}RLP97KbLq9tiS11p{= zJTM2TZTodV|0y)#M&=dbcTF=6s9Pw%b%U@VtwV)8Cv%A*>j253?!P zq%&?%deg?%=Q7N&AFf&Np)^n?W~KC$;&|3p%KHHayZ)zlbZTexRE2p9mj3voi@~lx zxV&W}frtcobC*Rd9ls~z+o@=}{fXC7F(1wa1z$Rua*I~kSJ3XX2h=HJ`gQQw%c_ms zHwr<}cY6LLvX0@&g{-njJ?lC!z~P767B)vEI%j7KLlnVtcS9wxUzV)f7Z%Z|4g<`m zNbR})&o8%doB4<7Dqx5XI7vfj`n|dh?hAF-W)~_`0O9*}ZAmlBjW;p~|Aataz$DU) z`N@F%fY;|qOShN!4GE_jEzH&g{o}=xF&gm1?QLj6Cq6Z;z-S@kzHI4Epe%2sfU2(L zq;(jECeF;wjw`p3K!TkJ20g^mLiOT>5d95X?7UXiL>Ujf9ckDeh=ZWtd?I5wHA}sZ za|26$mLK95?N!-I3$B||iv~x+jWvLKJRS4fq=*xwY zR&bSrZVsr3Gu+|2n!cV8uI|udqzW+5AI=E}d3f1OtN+0N;pG1(Ab#CL36azT_gYo> zyUgX>E0GMTx>%pP-r&W>9f#neY(pJ%=MA^C{HdT5lxSl5@#9nwSqlTQyiBsYT3Qv zYu)#dk~_gf!>-vL&Epk^rp@%IHO7war2J{M^)JKk$1Bp495ui(rBGQp-56 zDk*UorYg%rIqVoyP!t;vPNBa|Z*O?KiZ&A18z{##fDQRSe+CW0U0f5TxM_@{C9H(E Lw6iF~dH(ZXPdp%T literal 6083 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 J%CTqv`Y%_WfCvBp From c6e64a3a5f200c0f222b56d19e671c2b602a5342 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 13:17:27 -0800 Subject: [PATCH 14/47] Allow Tor Caching + Our Own Linkify --- lib/l10n/intl_de.arb | 4 +- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_es.arb | 4 +- lib/l10n/intl_fr.arb | 4 +- lib/l10n/intl_it.arb | 4 +- lib/l10n/intl_pl.arb | 4 +- lib/l10n/intl_pt.arb | 4 +- lib/l10n/intl_ru.arb | 4 +- lib/settings.dart | 16 + lib/third_party/linkify/flutter_linkify.dart | 380 +++++++++++++++++++ lib/third_party/linkify/linkify.dart | 128 +++++++ lib/third_party/linkify/uri.dart | 127 +++++++ lib/views/torstatusview.dart | 12 + lib/widgets/messagebubble.dart | 4 +- pubspec.lock | 14 - pubspec.yaml | 1 - 16 files changed, 690 insertions(+), 24 deletions(-) create mode 100644 lib/third_party/linkify/flutter_linkify.dart create mode 100644 lib/third_party/linkify/linkify.dart create mode 100644 lib/third_party/linkify/uri.dart diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 1d4bcc7a..ff53b0ac 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,8 @@ { "@@locale": "de", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b38690f5..0e1cb975 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,8 @@ { "@@locale": "en", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 403fc323..81fd56f3 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,8 @@ { "@@locale": "es", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index dc576196..82f8c123 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,8 @@ { "@@locale": "fr", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 43902cb0..2a69d62d 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,8 @@ { "@@locale": "it", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index f66ea23e..882e91de 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,8 @@ { "@@locale": "pl", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 95ac2390..fa60f581 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,8 @@ { "@@locale": "pt", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 0a1f3102..4ea0b04e 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,8 @@ { "@@locale": "ru", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/settings.dart b/lib/settings.dart index 343a374d..621018c2 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -45,6 +45,10 @@ class Settings extends ChangeNotifier { int _socksPort = -1; int _controlPort = -1; String _customTorAuth = ""; + bool _useTorCache = false; + String _torCacheDir = ""; + + String get torCacheDir => _torCacheDir; void setTheme(String themeId, String mode) { theme = getTheme(themeId, mode); @@ -99,6 +103,8 @@ class Settings extends ChangeNotifier { _customTorConfig = settings["CustomTorrc"] ?? ""; _socksPort = settings["CustomSocksPort"] ?? -1; _controlPort = settings["CustomControlPort"] ?? -1; + _useTorCache = settings["UseTorCache"] ?? false; + _torCacheDir = settings["TorCacheDir"] ?? ""; // Push the experimental settings to Consumers of Settings notifyListeners(); @@ -252,6 +258,12 @@ class Settings extends ChangeNotifier { notifyListeners(); } + bool get useTorCache => _useTorCache; + set useTorCache(bool useTorCache) { + _useTorCache = useTorCache; + notifyListeners(); + } + // Settings / Gettings for setting the custom tor config.. String get torConfig => _customTorConfig; set torConfig(String torConfig) { @@ -304,6 +316,10 @@ class Settings extends ChangeNotifier { "CustomSocksPort": _socksPort, "CustomControlPort": _controlPort, "CustomAuth": _customTorAuth, + "UseTorCache": _useTorCache, + "TorCacheDir": _torCacheDir }; } } + + diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart new file mode 100644 index 00000000..0db37816 --- /dev/null +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -0,0 +1,380 @@ +// +// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ and +// subsequently modified... +// Original License for this code: +// MIT License +// Copyright (c) 2020 Charles-William Crete +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'linkify.dart'; + + + +/// Callback clicked link +typedef LinkCallback = void Function(LinkableElement link); + +/// Turns URLs into links +class Linkify extends StatelessWidget { + /// Text to be linkified + final String text; + + /// Linkifiers to be used for linkify + final List linkifiers; + + /// Callback for tapping a link + final LinkCallback? onOpen; + + /// linkify's options. + final LinkifyOptions options; + + // TextSpan + + /// Style for non-link text + final TextStyle? style; + + /// Style of link text + final TextStyle? linkStyle; + + // Text.rich + + /// How the text should be aligned horizontally. + final TextAlign textAlign; + + /// Text direction of the text + final TextDirection? textDirection; + + /// The maximum number of lines for the text to span, wrapping if necessary + final int? maxLines; + + /// How visual overflow should be handled. + final TextOverflow overflow; + + /// The number of font pixels for each logical pixel + final double textScaleFactor; + + /// Whether the text should break at soft line breaks. + final bool softWrap; + + /// The strut style used for the vertical layout + final StrutStyle? strutStyle; + + /// Used to select a font when the same Unicode character can + /// be rendered differently, depending on the locale + final Locale? locale; + + /// Defines how to measure the width of the rendered text. + final TextWidthBasis textWidthBasis; + + /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line. + final TextHeightBehavior? textHeightBehavior; + + const Linkify({ + Key? key, + required this.text, + this.linkifiers = defaultLinkifiers, + this.onOpen, + this.options = const LinkifyOptions(), + // TextSpan + this.style, + this.linkStyle, + // RichText + this.textAlign = TextAlign.start, + this.textDirection, + this.maxLines, + this.overflow = TextOverflow.clip, + this.textScaleFactor = 1.0, + this.softWrap = true, + this.strutStyle, + this.locale, + this.textWidthBasis = TextWidthBasis.parent, + this.textHeightBehavior, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final elements = linkify( + text, + options: options, + linkifiers: linkifiers, + ); + + return Text.rich( + buildTextSpan( + elements, + style: Theme.of(context).textTheme.bodyText2?.merge(style), + onOpen: onOpen, + useMouseRegion: true, + linkStyle: Theme.of(context) + .textTheme + .bodyText2 + ?.merge(style) + .copyWith( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) + .merge(linkStyle), + ), + textAlign: textAlign, + textDirection: textDirection, + maxLines: maxLines, + overflow: overflow, + textScaleFactor: textScaleFactor, + softWrap: softWrap, + strutStyle: strutStyle, + locale: locale, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + ); + } +} + +/// Turns URLs into links +class SelectableLinkify extends StatelessWidget { + /// Text to be linkified + final String text; + + /// The number of font pixels for each logical pixel + final textScaleFactor; + + /// Linkifiers to be used for linkify + final List linkifiers; + + /// Callback for tapping a link + final LinkCallback? onOpen; + + /// linkify's options. + final LinkifyOptions options; + + // TextSpan + + /// Style for non-link text + final TextStyle? style; + + /// Style of link text + final TextStyle? linkStyle; + + // Text.rich + + /// How the text should be aligned horizontally. + final TextAlign? textAlign; + + /// Text direction of the text + final TextDirection? textDirection; + + /// The minimum number of lines to occupy when the content spans fewer lines. + final int? minLines; + + /// The maximum number of lines for the text to span, wrapping if necessary + final int? maxLines; + + /// The strut style used for the vertical layout + final StrutStyle? strutStyle; + + /// Defines how to measure the width of the rendered text. + final TextWidthBasis? textWidthBasis; + + // SelectableText.rich + + /// Defines the focus for this widget. + final FocusNode? focusNode; + + /// Whether to show cursor + final bool showCursor; + + /// Whether this text field should focus itself if nothing else is already focused. + final bool autofocus; + + /// Configuration of toolbar options + final ToolbarOptions? toolbarOptions; + + /// How thick the cursor will be + final double cursorWidth; + + /// How rounded the corners of the cursor should be + final Radius? cursorRadius; + + /// The color to use when painting the cursor + final Color? cursorColor; + + /// Determines the way that drag start behavior is handled + final DragStartBehavior dragStartBehavior; + + /// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu, + /// and tapping will move the text caret + final bool enableInteractiveSelection; + + /// Called when the user taps on this selectable text (not link) + final GestureTapCallback? onTap; + + final ScrollPhysics? scrollPhysics; + + /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line. + final TextHeightBehavior? textHeightBehavior; + + /// How tall the cursor will be. + final double? cursorHeight; + + /// Optional delegate for building the text selection handles and toolbar. + final TextSelectionControls? selectionControls; + + /// Called when the user changes the selection of text (including the cursor location). + final SelectionChangedCallback? onSelectionChanged; + + const SelectableLinkify({ + Key? key, + required this.text, + this.linkifiers = defaultLinkifiers, + this.onOpen, + this.options = const LinkifyOptions(), + // TextSpan + this.style, + this.linkStyle, + // RichText + this.textAlign, + this.textDirection, + this.minLines, + this.maxLines, + // SelectableText + this.focusNode, + this.textScaleFactor = 1.0, + this.strutStyle, + this.showCursor = false, + this.autofocus = false, + this.toolbarOptions, + this.cursorWidth = 2.0, + this.cursorRadius, + this.cursorColor, + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.onTap, + this.scrollPhysics, + this.textWidthBasis, + this.textHeightBehavior, + this.cursorHeight, + this.selectionControls, + this.onSelectionChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final elements = linkify( + text, + options: options, + linkifiers: linkifiers, + ); + + return SelectableText.rich( + buildTextSpan( + elements, + style: Theme.of(context).textTheme.bodyText2?.merge(style), + onOpen: onOpen, + linkStyle: Theme.of(context) + .textTheme + .bodyText2 + ?.merge(style) + .copyWith( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) + .merge(linkStyle), + ), + textAlign: textAlign, + textDirection: textDirection, + minLines: minLines, + maxLines: maxLines, + focusNode: focusNode, + strutStyle: strutStyle, + showCursor: showCursor, + textScaleFactor: textScaleFactor, + autofocus: autofocus, + toolbarOptions: toolbarOptions, + cursorWidth: cursorWidth, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + dragStartBehavior: dragStartBehavior, + enableInteractiveSelection: enableInteractiveSelection, + onTap: onTap, + scrollPhysics: scrollPhysics, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + cursorHeight: cursorHeight, + selectionControls: selectionControls, + onSelectionChanged: onSelectionChanged, + ); + } +} + +class LinkableSpan extends WidgetSpan { + LinkableSpan({ + required MouseCursor mouseCursor, + required InlineSpan inlineSpan, + }) : super( + child: MouseRegion( + cursor: mouseCursor, + child: Text.rich( + inlineSpan, + ), + ), + ); +} + +/// Raw TextSpan builder for more control on the RichText +TextSpan buildTextSpan( + List elements, { + TextStyle? style, + TextStyle? linkStyle, + LinkCallback? onOpen, + bool useMouseRegion = false, + }) { + return TextSpan( + children: elements.map( + (element) { + if (element is LinkableElement) { + if (useMouseRegion) { + return LinkableSpan( + mouseCursor: SystemMouseCursors.click, + inlineSpan: TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ), + ); + } else { + return TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ); + } + } else { + return TextSpan( + text: element.text, + style: style, + ); + } + }, + ).toList(), + ); +} diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart new file mode 100644 index 00000000..ea6f027a --- /dev/null +++ b/lib/third_party/linkify/linkify.dart @@ -0,0 +1,128 @@ +// Originally from linkify +// MIT License +// +// Copyright (c) 2019 Charles-William Crete +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:cwtch/third_party/linkify/uri.dart'; + +abstract class LinkifyElement { + final String text; + + LinkifyElement(this.text); + + @override + bool operator ==(other) => equals(other); + + bool equals(other) => other is LinkifyElement && other.text == text; +} + +class LinkableElement extends LinkifyElement { + final String url; + + LinkableElement(String? text, this.url) : super(text ?? url); + + @override + bool operator ==(other) => equals(other); + + @override + bool equals(other) => + other is LinkableElement && super.equals(other) && other.url == url; +} + +/// Represents an element containing text +class TextElement extends LinkifyElement { + TextElement(String text) : super(text); + + @override + String toString() { + return "TextElement: '$text'"; + } + + @override + bool operator ==(other) => equals(other); + + @override + bool equals(other) => other is TextElement && super.equals(other); +} + +abstract class Linkifier { + const Linkifier(); + + List parse( + List elements, LinkifyOptions options); +} + +class LinkifyOptions { + /// Removes http/https from shown URLs. + final bool humanize; + + /// Removes www. from shown URLs. + final bool removeWww; + + /// Enables loose URL parsing (any string with "." is a URL). + final bool looseUrl; + + /// When used with [looseUrl], default to `https` instead of `http`. + final bool defaultToHttps; + + /// Excludes `.` at end of URLs. + final bool excludeLastPeriod; + + const LinkifyOptions({ + this.humanize = true, + this.removeWww = false, + this.looseUrl = false, + this.defaultToHttps = false, + this.excludeLastPeriod = true, + }); +} + +const _urlLinkifier = UrlLinkifier(); +const defaultLinkifiers = [_urlLinkifier]; + +/// Turns [text] into a list of [LinkifyElement] +/// +/// Use [humanize] to remove http/https from the start of the URL shown. +/// Will default to `false` (if `null`) +/// +/// Uses [linkTypes] to enable some types of links (URL, email). +/// Will default to all (if `null`). +List linkify( + String text, { + LinkifyOptions options = const LinkifyOptions(), + List linkifiers = defaultLinkifiers, + }) { + var list = [TextElement(text)]; + + if (text.isEmpty) { + return []; + } + + if (linkifiers.isEmpty) { + return list; + } + + linkifiers.forEach((linkifier) { + list = linkifier.parse(list, options); + }); + + return list; +} diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart new file mode 100644 index 00000000..4c432f09 --- /dev/null +++ b/lib/third_party/linkify/uri.dart @@ -0,0 +1,127 @@ +// Originally from linkify +// MIT License +// +// Copyright (c) 2019 Charles-William Crete +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:cwtch/third_party/linkify/linkify.dart'; + +final _urlRegex = RegExp( + r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', + caseSensitive: false, + dotAll: true, +); + +final _looseUrlRegex = RegExp( + r'^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))', + caseSensitive: false, + dotAll: true, +); + +final _protocolIdentifierRegex = RegExp( + r'^(https?:\/\/)', + caseSensitive: false, +); + +class UrlLinkifier extends Linkifier { + const UrlLinkifier(); + + @override + List parse(elements, options) { + final list = []; + + elements.forEach((element) { + if (element is TextElement) { + var match = options.looseUrl + ? _looseUrlRegex.firstMatch(element.text) + : _urlRegex.firstMatch(element.text); + + if (match == null) { + list.add(element); + } else { + final text = element.text.replaceFirst(match.group(0)!, ''); + + if (match.group(1)?.isNotEmpty == true) { + list.add(TextElement(match.group(1)!)); + } + + if (match.group(2)?.isNotEmpty == true) { + var originalUrl = match.group(2)!; + String? end; + + if ((options.excludeLastPeriod) && + originalUrl[originalUrl.length - 1] == ".") { + end = "."; + originalUrl = originalUrl.substring(0, originalUrl.length - 1); + } + + var url = originalUrl; + + // We do not, ever, change the original text of a message. + if (options.defaultToHttps) { + url = url.replaceFirst('http://', 'https://'); + } + + // These options are intended for the human-readable portion of + // the URI + if (options.humanize) { + originalUrl = originalUrl.replaceFirst(RegExp(r'https?://'), ''); + } + + if (options.removeWww) { + originalUrl = originalUrl.replaceFirst(RegExp(r'www\.'), ''); + } + + list.add(UrlElement(originalUrl, url)); + + + if (end != null) { + list.add(TextElement(end)); + } + } + + if (text.isNotEmpty) { + list.addAll(parse([TextElement(text)], options)); + } + } + } else { + list.add(element); + } + }); + + return list; + } +} + +/// Represents an element containing a link +class UrlElement extends LinkableElement { + UrlElement(String url, [String? text]) : super(text, url); + + @override + String toString() { + return "LinkElement: '$url' ($text)"; + } + + @override + bool operator ==(other) => equals(other); + + @override + bool equals(other) => other is UrlElement && super.equals(other); +} diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 982bfbaa..ed2ac999 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -76,6 +76,18 @@ class _TorStatusView extends State { subtitle: SelectableText(torStatus.version), leading: Icon(CwtchIcons.info_24px, color: settings.current().mainTextColor), ), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.torSettingsEnableCache), + subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledCacheDescription), + value: settings.useTorCache, + onChanged: (bool value) { + settings.useTorCache = value; + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(Icons.cached, color: settings.current().mainTextColor), + ), SwitchListTile( title: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvanced), subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvancedDescription), diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 469a1b5a..f1a26eee 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -1,6 +1,9 @@ import 'dart:io'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/third_party/linkify/flutter_linkify.dart'; +import 'package:cwtch/third_party/linkify/linkify.dart'; +import 'package:cwtch/third_party/linkify/uri.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +11,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import '../model.dart'; import 'package:intl/intl.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import '../settings.dart'; diff --git a/pubspec.lock b/pubspec.lock index 3c1db18c..a967a6be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,13 +125,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - url: "https://pub.dartlang.org" - source: hosted - version: "5.0.2" flutter_localizations: dependency: "direct main" description: flutter @@ -196,13 +189,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" - linkify: - dependency: transitive - description: - name: linkify - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c93ac51..03d6d590 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,6 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 file_picker: ^4.0.1 file_picker_desktop: ^1.1.0 - flutter_linkify: ^5.0.2 url_launcher: ^6.0.12 dev_dependencies: From b3f06d6765dfdb276d2a8db45491424bedb7e353 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 13:47:47 -0800 Subject: [PATCH 15/47] Update lcg --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index c1538941..b8bbdfcc 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-17-17-19-v1.5.4-5-g4cf95d6 \ No newline at end of file +2022-01-18-16-33-v1.5.4-8-g2aea700 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index ed75fff0..2ea2c4de 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-17-22-19-v1.5.4-5-g4cf95d6 \ No newline at end of file +2022-01-18-21-29-v1.5.4-8-g2aea700 \ No newline at end of file From cd1bf07fba3bbf9f4916f2464ce695989fd8b62c Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:32:45 -0800 Subject: [PATCH 16/47] Responding to @errorinn PR Comments --- lib/settings.dart | 2 - lib/third_party/linkify/flutter_linkify.dart | 48 ++++++++++---------- lib/third_party/linkify/linkify.dart | 26 ++++------- lib/third_party/linkify/uri.dart | 29 +++--------- lib/widgets/messagebubble.dart | 4 +- 5 files changed, 41 insertions(+), 68 deletions(-) diff --git a/lib/settings.dart b/lib/settings.dart index 621018c2..8bfaa3f9 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -321,5 +321,3 @@ class Settings extends ChangeNotifier { }; } } - - diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 0db37816..4e702841 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -1,6 +1,7 @@ +// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ +// +// Now uses local `linkify` // -// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ and -// subsequently modified... // Original License for this code: // MIT License // Copyright (c) 2020 Charles-William Crete @@ -23,14 +24,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'linkify.dart'; - +export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier; /// Callback clicked link typedef LinkCallback = void Function(LinkableElement link); @@ -131,9 +131,9 @@ class Linkify extends StatelessWidget { .bodyText2 ?.merge(style) .copyWith( - color: Colors.blueAccent, - decoration: TextDecoration.underline, - ) + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) .merge(linkStyle), ), textAlign: textAlign, @@ -295,9 +295,9 @@ class SelectableLinkify extends StatelessWidget { .bodyText2 ?.merge(style) .copyWith( - color: Colors.blueAccent, - decoration: TextDecoration.underline, - ) + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) .merge(linkStyle), ), textAlign: textAlign, @@ -331,26 +331,26 @@ class LinkableSpan extends WidgetSpan { required MouseCursor mouseCursor, required InlineSpan inlineSpan, }) : super( - child: MouseRegion( - cursor: mouseCursor, - child: Text.rich( - inlineSpan, - ), - ), - ); + child: MouseRegion( + cursor: mouseCursor, + child: Text.rich( + inlineSpan, + ), + ), + ); } /// Raw TextSpan builder for more control on the RichText TextSpan buildTextSpan( - List elements, { - TextStyle? style, - TextStyle? linkStyle, - LinkCallback? onOpen, - bool useMouseRegion = false, - }) { + List elements, { + TextStyle? style, + TextStyle? linkStyle, + LinkCallback? onOpen, + bool useMouseRegion = false, +}) { return TextSpan( children: elements.map( - (element) { + (element) { if (element is LinkableElement) { if (useMouseRegion) { return LinkableSpan( diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index ea6f027a..381babb6 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -1,4 +1,6 @@ -// Originally from linkify +// Originally from linkify https://github.com/Cretezy/linkify/blob/master/lib/linkify.dart +// Removed options `removeWWW` and `humanize` +// // MIT License // // Copyright (c) 2019 Charles-William Crete @@ -43,8 +45,7 @@ class LinkableElement extends LinkifyElement { bool operator ==(other) => equals(other); @override - bool equals(other) => - other is LinkableElement && super.equals(other) && other.url == url; + bool equals(other) => other is LinkableElement && super.equals(other) && other.url == url; } /// Represents an element containing text @@ -66,17 +67,10 @@ class TextElement extends LinkifyElement { abstract class Linkifier { const Linkifier(); - List parse( - List elements, LinkifyOptions options); + List parse(List elements, LinkifyOptions options); } class LinkifyOptions { - /// Removes http/https from shown URLs. - final bool humanize; - - /// Removes www. from shown URLs. - final bool removeWww; - /// Enables loose URL parsing (any string with "." is a URL). final bool looseUrl; @@ -87,8 +81,6 @@ class LinkifyOptions { final bool excludeLastPeriod; const LinkifyOptions({ - this.humanize = true, - this.removeWww = false, this.looseUrl = false, this.defaultToHttps = false, this.excludeLastPeriod = true, @@ -106,10 +98,10 @@ const defaultLinkifiers = [_urlLinkifier]; /// Uses [linkTypes] to enable some types of links (URL, email). /// Will default to all (if `null`). List linkify( - String text, { - LinkifyOptions options = const LinkifyOptions(), - List linkifiers = defaultLinkifiers, - }) { + String text, { + LinkifyOptions options = const LinkifyOptions(), + List linkifiers = defaultLinkifiers, +}) { var list = [TextElement(text)]; if (text.isEmpty) { diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 4c432f09..7cca8072 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -1,4 +1,8 @@ -// Originally from linkify +// Originally from linkify: https://github.com/Cretezy/linkify/blob/master/lib/src/url.dart +// +// Removed handling of `removeWWW` and `humanize`. +// Removed auto-appending of `http(s)://` to the readable url +// // MIT License // // Copyright (c) 2019 Charles-William Crete @@ -49,9 +53,7 @@ class UrlLinkifier extends Linkifier { elements.forEach((element) { if (element is TextElement) { - var match = options.looseUrl - ? _looseUrlRegex.firstMatch(element.text) - : _urlRegex.firstMatch(element.text); + var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text); if (match == null) { list.add(element); @@ -66,32 +68,15 @@ class UrlLinkifier extends Linkifier { var originalUrl = match.group(2)!; String? end; - if ((options.excludeLastPeriod) && - originalUrl[originalUrl.length - 1] == ".") { + if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") { end = "."; originalUrl = originalUrl.substring(0, originalUrl.length - 1); } var url = originalUrl; - // We do not, ever, change the original text of a message. - if (options.defaultToHttps) { - url = url.replaceFirst('http://', 'https://'); - } - - // These options are intended for the human-readable portion of - // the URI - if (options.humanize) { - originalUrl = originalUrl.replaceFirst(RegExp(r'https?://'), ''); - } - - if (options.removeWww) { - originalUrl = originalUrl.replaceFirst(RegExp(r'www\.'), ''); - } - list.add(UrlElement(originalUrl, url)); - if (end != null) { list.add(TextElement(end)); } diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index f1a26eee..948351cd 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -2,8 +2,6 @@ import 'dart:io'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/third_party/linkify/flutter_linkify.dart'; -import 'package:cwtch/third_party/linkify/linkify.dart'; -import 'package:cwtch/third_party/linkify/uri.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -69,7 +67,7 @@ class MessageBubbleState extends State { wdgMessage = SelectableLinkify( text: widget.content + '\u202F', // TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler? - options: LinkifyOptions(humanize: false, removeWww: false, looseUrl: true, defaultToHttps: true), + options: LinkifyOptions(looseUrl: true, defaultToHttps: true), linkifiers: [UrlLinkifier()], onOpen: (link) { _modalOpenLink(context, link); From 303b70d75162aad2dc799ab0094df1409f91703f Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:43:49 -0800 Subject: [PATCH 17/47] Fixup displayed link + add linkify to licenses.dart --- lib/licenses.dart | 23 ++++++++++++++++++++ lib/third_party/linkify/flutter_linkify.dart | 2 +- lib/third_party/linkify/linkify.dart | 5 ++++- lib/third_party/linkify/uri.dart | 10 +++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/licenses.dart b/lib/licenses.dart index a665eed0..cc4194c8 100644 --- a/lib/licenses.dart +++ b/lib/licenses.dart @@ -116,4 +116,27 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''); yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)"); + + yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], + '''MIT License + +Copyright (c) 2019/2020 Charles-William Crete + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.'''); } diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 4e702841..28638e77 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -30,7 +30,7 @@ import 'package:flutter/rendering.dart'; import 'linkify.dart'; -export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier; +export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier, UrlElement, UrlLinkifier; /// Callback clicked link typedef LinkCallback = void Function(LinkableElement link); diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index 381babb6..9bde6bdd 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -23,7 +23,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import 'package:cwtch/third_party/linkify/uri.dart'; + +import 'uri.dart'; +export 'uri.dart' show UrlLinkifier, UrlElement; + abstract class LinkifyElement { final String text; diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 7cca8072..8d0e2874 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -25,7 +25,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import 'package:cwtch/third_party/linkify/linkify.dart'; +import 'linkify.dart'; final _urlRegex = RegExp( r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', @@ -75,7 +75,13 @@ class UrlLinkifier extends Linkifier { var url = originalUrl; - list.add(UrlElement(originalUrl, url)); + // If protocol has not been specified then append a protocol + // to the start of the URL so that it can be opened... + if (!url.startsWith("https://") && !url.startsWith("http://")) { + url = "https://"+url; + } + + list.add(UrlElement(url, originalUrl)); if (end != null) { list.add(TextElement(end)); From da3234e3e4926529961ee6f13e0aad7b3674e08d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:44:19 -0800 Subject: [PATCH 18/47] Formatting --- lib/licenses.dart | 3 +-- lib/third_party/linkify/linkify.dart | 2 -- lib/third_party/linkify/uri.dart | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/licenses.dart b/lib/licenses.dart index cc4194c8..057f5c80 100644 --- a/lib/licenses.dart +++ b/lib/licenses.dart @@ -117,8 +117,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''); yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)"); - yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], - '''MIT License + yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], '''MIT License Copyright (c) 2019/2020 Charles-William Crete diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index 9bde6bdd..d4355313 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -23,11 +23,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - import 'uri.dart'; export 'uri.dart' show UrlLinkifier, UrlElement; - abstract class LinkifyElement { final String text; diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 8d0e2874..4220794c 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -78,7 +78,7 @@ class UrlLinkifier extends Linkifier { // If protocol has not been specified then append a protocol // to the start of the URL so that it can be opened... if (!url.startsWith("https://") && !url.startsWith("http://")) { - url = "https://"+url; + url = "https://" + url; } list.add(UrlElement(url, originalUrl)); From 1700306c787d3e4298fa91f67644f843da981ec9 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:48:18 -0800 Subject: [PATCH 19/47] Link to specific commit hashes --- lib/third_party/linkify/flutter_linkify.dart | 2 +- lib/third_party/linkify/linkify.dart | 2 +- lib/third_party/linkify/uri.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 28638e77..9be50666 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -1,4 +1,4 @@ -// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ +// Code Originally taken from https://github.com/Cretezy/flutter_linkify/blob/201e147e0b07b7ca5c543da8167d712d81760753/lib/flutter_linkify.dart // // Now uses local `linkify` // diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index d4355313..7a7a0248 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -1,4 +1,4 @@ -// Originally from linkify https://github.com/Cretezy/linkify/blob/master/lib/linkify.dart +// Originally from linkify https://github.com/Cretezy/linkify/blob/ba536fa85e7e3a16e580f153616f399458986183/lib/linkify.dart // Removed options `removeWWW` and `humanize` // // MIT License diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 4220794c..9df90bdd 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -1,4 +1,4 @@ -// Originally from linkify: https://github.com/Cretezy/linkify/blob/master/lib/src/url.dart +// Originally from linkify: https://github.com/Cretezy/linkify/blob/dfb3e43b0e56452bad584ddb0bf9b73d8db0589f/lib/src/url.dart // // Removed handling of `removeWWW` and `humanize`. // Removed auto-appending of `http(s)://` to the readable url From ca44fd798c914c67ac8ac0eeb3eda95316b4f907 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 15:03:54 -0800 Subject: [PATCH 20/47] Show tooltip for links --- lib/third_party/linkify/flutter_linkify.dart | 45 ++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 9be50666..acf82588 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -353,20 +353,24 @@ TextSpan buildTextSpan( (element) { if (element is LinkableElement) { if (useMouseRegion) { - return LinkableSpan( - mouseCursor: SystemMouseCursors.click, - inlineSpan: TextSpan( - text: element.text, - style: linkStyle, - recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, - ), - ); + return TooltipSpan( + message: element.url, + inlineSpan: LinkableSpan( + mouseCursor: SystemMouseCursors.click, + inlineSpan: TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ), + )); } else { - return TextSpan( - text: element.text, - style: linkStyle, - recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, - ); + return TooltipSpan( + message: element.url, + inlineSpan: TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + )); } } else { return TextSpan( @@ -378,3 +382,18 @@ TextSpan buildTextSpan( ).toList(), ); } + +// Show a tooltip over an inlined element in a Rich Text widget. +class TooltipSpan extends WidgetSpan { + TooltipSpan({ + required String message, + required InlineSpan inlineSpan, + }) : super( + child: Tooltip( + message: message, + child: Text.rich( + inlineSpan, + ), + ), + ); +} From 706c1fb3540c22d21fa9a1a2ca7110cf52ad5b74 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 18 Jan 2022 16:26:52 -0500 Subject: [PATCH 21/47] move all classes in model.dart to their own models/X.dart --- lib/cwtch/cwtchNotifier.dart | 4 +- lib/main.dart | 3 +- lib/model.dart | 751 ------------------------- lib/models/appstate.dart | 71 +++ lib/models/chatmessage.dart | 15 + lib/models/contact.dart | 214 +++++++ lib/models/contactlist.dart | 125 ++++ lib/models/filedownloadprogress.dart | 27 + lib/models/message.dart | 2 +- lib/models/messagecache.dart | 7 + lib/models/messages/filemessage.dart | 2 +- lib/models/messages/invitemessage.dart | 2 +- lib/models/messages/quotedmessage.dart | 1 - lib/models/messages/textmessage.dart | 2 - lib/models/profile.dart | 272 +++++++++ lib/models/profilelist.dart | 30 + lib/models/profileservers.dart | 3 +- lib/views/addcontactview.dart | 2 +- lib/views/addeditprofileview.dart | 2 +- lib/views/contactsview.dart | 6 +- lib/views/doublecolview.dart | 4 +- lib/views/groupsettingsview.dart | 4 +- lib/views/messageview.dart | 5 +- lib/views/peersettingsview.dart | 3 +- lib/views/profilemgrview.dart | 4 +- lib/views/profileserversview.dart | 2 +- lib/views/remoteserverview.dart | 3 +- lib/views/splashView.dart | 2 +- lib/widgets/DropdownContacts.dart | 3 +- lib/widgets/contactrow.dart | 4 +- lib/widgets/filebubble.dart | 4 +- lib/widgets/invitationbubble.dart | 3 +- lib/widgets/messagebubble.dart | 3 +- lib/widgets/messagelist.dart | 4 +- lib/widgets/messagerow.dart | 4 +- lib/widgets/profilerow.dart | 4 +- lib/widgets/quotedmessage.dart | 3 +- lib/widgets/remoteserverrow.dart | 2 +- lib/widgets/serverrow.dart | 1 - 39 files changed, 820 insertions(+), 783 deletions(-) delete mode 100644 lib/model.dart create mode 100644 lib/models/appstate.dart create mode 100644 lib/models/chatmessage.dart create mode 100644 lib/models/contact.dart create mode 100644 lib/models/contactlist.dart create mode 100644 lib/models/filedownloadprogress.dart create mode 100644 lib/models/messagecache.dart create mode 100644 lib/models/profile.dart create mode 100644 lib/models/profilelist.dart diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index f400f910..ef270f90 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,6 +1,9 @@ import 'dart:convert'; import 'package:cwtch/main.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profilelist.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/notification_manager.dart'; @@ -10,7 +13,6 @@ import 'package:cwtch/torstatus.dart'; import '../config.dart'; import '../errorHandler.dart'; -import '../model.dart'; import '../settings.dart'; // Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin) diff --git a/lib/main.dart b/lib/main.dart index 5d307a16..664464e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,8 @@ import 'package:provider/provider.dart'; import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; import 'licenses.dart'; -import 'model.dart'; +import 'models/appstate.dart'; +import 'models/profilelist.dart'; import 'models/servers.dart'; import 'views/profilemgrview.dart'; import 'views/splashView.dart'; diff --git a/lib/model.dart b/lib/model.dart deleted file mode 100644 index 00ebba2e..00000000 --- a/lib/model.dart +++ /dev/null @@ -1,751 +0,0 @@ -import 'dart:convert'; - -import 'package:cwtch/config.dart'; -import 'package:cwtch/models/message.dart'; -import 'package:cwtch/widgets/messagerow.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:cwtch/models/profileservers.dart'; - -//////////////////// -/// UI State /// -//////////////////// - -class ChatMessage { - final int o; - final String d; - - ChatMessage({required this.o, required this.d}); - - ChatMessage.fromJson(Map json) - : o = json['o'], - d = json['d']; - - Map toJson() => { - 'o': o, - 'd': d, - }; -} - -enum ModalState { none, storageMigration } - -class AppState extends ChangeNotifier { - bool cwtchInit = false; - ModalState modalState = ModalState.none; - bool cwtchIsClosing = false; - String appError = ""; - String? _selectedProfile; - int? _selectedConversation; - int _initialScrollIndex = 0; - int _hoveredIndex = -1; - int? _selectedIndex; - bool _unreadMessagesBelow = false; - - void SetCwtchInit() { - cwtchInit = true; - notifyListeners(); - } - - void SetAppError(String error) { - appError = error; - notifyListeners(); - } - - void SetModalState(ModalState newState) { - modalState = newState; - notifyListeners(); - } - - String? get selectedProfile => _selectedProfile; - set selectedProfile(String? newVal) { - this._selectedProfile = newVal; - notifyListeners(); - } - - int? get selectedConversation => _selectedConversation; - set selectedConversation(int? newVal) { - this._selectedConversation = newVal; - notifyListeners(); - } - - int? get selectedIndex => _selectedIndex; - set selectedIndex(int? newVal) { - this._selectedIndex = newVal; - notifyListeners(); - } - - // Never use this for message lookup - can be a non-indexed value - // e.g. -1 - int get hoveredIndex => _hoveredIndex; - set hoveredIndex(int newVal) { - this._hoveredIndex = newVal; - notifyListeners(); - } - - bool get unreadMessagesBelow => _unreadMessagesBelow; - set unreadMessagesBelow(bool newVal) { - this._unreadMessagesBelow = newVal; - notifyListeners(); - } - - int get initialScrollIndex => _initialScrollIndex; - set initialScrollIndex(int newVal) { - this._initialScrollIndex = newVal; - notifyListeners(); - } - - bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; -} - -/////////////////// -/// Providers /// -/////////////////// - -class ProfileListState extends ChangeNotifier { - List _profiles = []; - int get num => _profiles.length; - - void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { - var idx = _profiles.indexWhere((element) => element.onion == onion); - if (idx == -1) { - _profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted)); - } else { - _profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online); - } - notifyListeners(); - } - - List get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier - - ProfileInfoState? getProfile(String onion) { - int idx = _profiles.indexWhere((element) => element.onion == onion); - return idx >= 0 ? _profiles[idx] : null; - } - - void delete(String onion) { - _profiles.removeWhere((element) => element.onion == onion); - notifyListeners(); - } -} - -class ContactListState extends ChangeNotifier { - ProfileServerListState? servers; - List _contacts = []; - String _filter = ""; - int get num => _contacts.length; - int get numFiltered => isFiltered ? filteredList().length : num; - bool get isFiltered => _filter != ""; - String get filter => _filter; - set filter(String newVal) { - _filter = newVal.toLowerCase(); - notifyListeners(); - } - - void connectServers(ProfileServerListState servers) { - this.servers = servers; - } - - List filteredList() { - if (!isFiltered) return contacts; - return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); - } - - void addAll(Iterable newContacts) { - _contacts.addAll(newContacts); - servers?.clearGroups(); - _contacts.forEach((contact) { - if (contact.isGroup) { - servers?.addGroup(contact); - } - }); - resort(); - notifyListeners(); - } - - void add(ContactInfoState newContact) { - _contacts.add(newContact); - if (newContact.isGroup) { - servers?.addGroup(newContact); - } - resort(); - notifyListeners(); - } - - void resort() { - _contacts.sort((ContactInfoState a, ContactInfoState b) { - // return -1 = a first in list - // return 1 = b first in list - - // blocked contacts last - if (a.isBlocked == true && b.isBlocked != true) return 1; - if (a.isBlocked != true && b.isBlocked == true) return -1; - // archive is next... - if (!a.isArchived && b.isArchived) return -1; - if (a.isArchived && !b.isArchived) return 1; - - // unapproved top - if (a.isInvitation && !b.isInvitation) return -1; - if (!a.isInvitation && b.isInvitation) return 1; - - // special sorting for contacts with no messages in either history - if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) { - // online contacts first - if (a.isOnline() && !b.isOnline()) return -1; - if (!a.isOnline() && b.isOnline()) return 1; - // finally resort to onion - return a.onion.toString().compareTo(b.onion.toString()); - } - // finally... most recent history first - if (a.lastMessageTime.millisecondsSinceEpoch == 0) return 1; - if (b.lastMessageTime.millisecondsSinceEpoch == 0) return -1; - return b.lastMessageTime.compareTo(a.lastMessageTime); - }); - // if(changed) { - notifyListeners(); - //} - } - - void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) { - var contact = getContact(forIdentifier); - if (contact == null) return; - - // Assert that the new time is after the current last message time AND that - // new message time is before the current time. - if (newMessageTime.isAfter(contact.lastMessageTime)) { - if (newMessageTime.isBefore(DateTime.now().toLocal())) { - contact.lastMessageTime = newMessageTime; - } else { - // Otherwise set the last message time to now... - contact.lastMessageTime = DateTime.now().toLocal(); - } - resort(); - } - } - - List get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier - - ContactInfoState? getContact(int identifier) { - int idx = _contacts.indexWhere((element) => element.identifier == identifier); - return idx >= 0 ? _contacts[idx] : null; - } - - void removeContact(int identifier) { - int idx = _contacts.indexWhere((element) => element.identifier == identifier); - if (idx >= 0) { - _contacts.removeAt(idx); - notifyListeners(); - } - } - - ContactInfoState? findContact(String byHandle) { - int idx = _contacts.indexWhere((element) => element.onion == byHandle); - return idx >= 0 ? _contacts[idx] : null; - } -} - -class ProfileInfoState extends ChangeNotifier { - ProfileServerListState _servers = ProfileServerListState(); - ContactListState _contacts = ContactListState(); - final String onion; - String _nickname = ""; - String _imagePath = ""; - int _unreadMessages = 0; - bool _online = false; - Map _downloads = Map(); - - // assume profiles are encrypted...this will be set to false - // in the constructor if the profile is encrypted with the defacto password. - bool _encrypted = true; - - ProfileInfoState({ - required this.onion, - nickname = "", - imagePath = "", - unreadMessages = 0, - contactsJson = "", - serversJson = "", - online = false, - encrypted = true, - }) { - this._nickname = nickname; - this._imagePath = imagePath; - this._unreadMessages = unreadMessages; - this._online = online; - this._encrypted = encrypted; - - _contacts.connectServers(this._servers); - - if (contactsJson != null && contactsJson != "" && contactsJson != "null") { - this.replaceServers(serversJson); - - List contacts = jsonDecode(contactsJson); - this._contacts.addAll(contacts.map((contact) { - return ContactInfoState(this.onion, contact["identifier"], contact["onion"], - nickname: contact["name"], - status: contact["status"], - imagePath: contact["picture"], - accepted: contact["accepted"], - blocked: contact["blocked"], - savePeerHistory: contact["saveConversationHistory"], - numMessages: contact["numMessages"], - numUnread: contact["numUnread"], - isGroup: contact["isGroup"], - server: contact["groupServer"], - archived: contact["isArchived"] == true, - lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]))); - })); - - // dummy set to invoke sort-on-load - if (this._contacts.num > 0) { - this._contacts.updateLastMessageTime(this._contacts._contacts.first.identifier, this._contacts._contacts.first.lastMessageTime); - } - } - } - - // Parse out the server list json into our server info state struct... - void replaceServers(String serversJson) { - if (serversJson != "" && serversJson != "null") { - List servers = jsonDecode(serversJson); - this._servers.replace(servers.map((server) { - // TODO Keys... - return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]); - })); - - this._contacts.contacts.forEach((contact) { - if (contact.isGroup) { - _servers.addGroup(contact); - } - }); - - notifyListeners(); - } - } - - // - void updateServerStatusCache(String server, String status) { - this._servers.updateServerState(server, status); - notifyListeners(); - } - - // Getters and Setters for Online Status - bool get isOnline => this._online; - set isOnline(bool newValue) { - this._online = newValue; - notifyListeners(); - } - - // Check encrypted status for profile info screen - bool get isEncrypted => this._encrypted; - - String get nickname => this._nickname; - set nickname(String newValue) { - this._nickname = newValue; - notifyListeners(); - } - - String get imagePath => this._imagePath; - set imagePath(String newVal) { - this._imagePath = newVal; - notifyListeners(); - } - - int get unreadMessages => this._unreadMessages; - set unreadMessages(int newVal) { - this._unreadMessages = newVal; - notifyListeners(); - } - - // Remove a contact from a list. Currently only used when rejecting a group invitation. - // Eventually will also be used for other removals. - void removeContact(String handle) { - int idx = this.contactList._contacts.indexWhere((element) => element.onion == handle); - this.contactList._contacts.removeAt(idx); - notifyListeners(); - } - - ContactListState get contactList => this._contacts; - ProfileServerListState get serverList => this._servers; - - @override - void dispose() { - super.dispose(); - print("profileinfostate.dispose()"); - } - - void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) { - this._nickname = name; - this._imagePath = picture; - this._online = online; - this.replaceServers(serverJson); - - if (contactsJson != null && contactsJson != "" && contactsJson != "null") { - List contacts = jsonDecode(contactsJson); - contacts.forEach((contact) { - var profileContact = this._contacts.getContact(contact["identifier"]); - if (profileContact != null) { - profileContact.status = contact["status"]; - profileContact.totalMessages = contact["numMessages"]; - profileContact.unreadMessages = contact["numUnread"]; - profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])); - } else { - this._contacts.add(ContactInfoState( - this.onion, - contact["identifier"], - contact["onion"], - nickname: contact["name"], - status: contact["status"], - imagePath: contact["picture"], - accepted: contact["accepted"], - blocked: contact["blocked"], - savePeerHistory: contact["saveConversationHistory"], - numMessages: contact["numMessages"], - numUnread: contact["numUnread"], - isGroup: contact["isGroup"], - server: contact["groupServer"], - lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), - )); - } - }); - } - this._contacts.resort(); - } - - void downloadInit(String fileKey, int numChunks) { - this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); - } - - void downloadUpdate(String fileKey, int progress, int numChunks) { - if (!downloadActive(fileKey)) { - this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); - if (progress < 0) { - this._downloads[fileKey]!.interrupted = true; - } - } else { - if (this._downloads[fileKey]!.interrupted) { - this._downloads[fileKey]!.interrupted = false; - } - this._downloads[fileKey]!.chunksDownloaded = progress; - this._downloads[fileKey]!.chunksTotal = numChunks; - } - notifyListeners(); - } - - void downloadMarkManifest(String fileKey) { - if (!downloadActive(fileKey)) { - this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); - } - this._downloads[fileKey]!.gotManifest = true; - notifyListeners(); - } - - void downloadMarkFinished(String fileKey, String finalPath) { - if (!downloadActive(fileKey)) { - // happens as a result of a CheckDownloadStatus call, - // invoked from a historical (timeline) download message - // so setting numChunks correctly shouldn't matter - this.downloadInit(fileKey, 1); - } - // only update if different - if (!this._downloads[fileKey]!.complete) { - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.downloadedTo = finalPath; - this._downloads[fileKey]!.complete = true; - notifyListeners(); - } - } - - bool downloadKnown(String fileKey) { - return this._downloads.containsKey(fileKey); - } - - bool downloadActive(String fileKey) { - return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted; - } - - bool downloadGotManifest(String fileKey) { - return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest; - } - - bool downloadComplete(String fileKey) { - return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; - } - - bool downloadInterrupted(String fileKey) { - return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted; - } - - void downloadMarkResumed(String fileKey) { - if (this._downloads.containsKey(fileKey)) { - this._downloads[fileKey]!.interrupted = false; - } - } - - double downloadProgress(String fileKey) { - return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; - } - - // used for loading interrupted download info; use downloadMarkFinished for successful downloads - void downloadSetPath(String fileKey, String path) { - if (this._downloads.containsKey(fileKey)) { - this._downloads[fileKey]!.downloadedTo = path; - } - } - - String? downloadFinalPath(String fileKey) { - return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; - } - - String downloadSpeed(String fileKey) { - if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) { - return "0 B/s"; - } - var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; - var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds; - if (seconds == 0) { - return "0 B/s"; - } - return prettyBytes((bytes / seconds).round()) + "/s"; - } -} - -class FileDownloadProgress { - int chunksDownloaded = 0; - int chunksTotal = 1; - bool complete = false; - bool gotManifest = false; - bool interrupted = false; - String? downloadedTo; - DateTime? timeStart; - DateTime? timeEnd; - - FileDownloadProgress(this.chunksTotal, this.timeStart); - double progress() { - return 1.0 * chunksDownloaded / chunksTotal; - } -} - -String prettyBytes(int bytes) { - if (bytes > 1000000000) { - return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB"; - } else if (bytes > 1000000) { - return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB"; - } else if (bytes > 1000) { - return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB"; - } else { - return bytes.toString() + " B"; - } -} - -class MessageCache { - final MessageMetadata metadata; - final String wrapper; - MessageCache(this.metadata, this.wrapper); -} - -class ContactInfoState extends ChangeNotifier { - final String profileOnion; - final int identifier; - final String onion; - late String _nickname; - - late bool _accepted; - late bool _blocked; - late String _status; - late String _imagePath; - late String _savePeerHistory; - late int _unreadMessages = 0; - late int _totalMessages = 0; - late DateTime _lastMessageTime; - late Map> keys; - late List messageCache; - int _newMarker = 0; - DateTime _newMarkerClearAt = DateTime.now(); - - // todo: a nicer way to model contacts, groups and other "entities" - late bool _isGroup; - String? _server; - late bool _archived; - - String? _acnCircuit; - - ContactInfoState(this.profileOnion, this.identifier, this.onion, - {nickname = "", - isGroup = false, - accepted = false, - blocked = false, - status = "", - imagePath = "", - savePeerHistory = "DeleteHistoryConfirmed", - numMessages = 0, - numUnread = 0, - lastMessageTime, - server, - archived = false}) { - this._nickname = nickname; - this._isGroup = isGroup; - this._accepted = accepted; - this._blocked = blocked; - this._status = status; - this._imagePath = imagePath; - this._totalMessages = numMessages; - this._unreadMessages = numUnread; - this._savePeerHistory = savePeerHistory; - this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; - this._server = server; - this._archived = archived; - this.messageCache = List.empty(growable: true); - keys = Map>(); - } - - String get nickname => this._nickname; - - String get savePeerHistory => this._savePeerHistory; - - String? get acnCircuit => this._acnCircuit; - set acnCircuit(String? acnCircuit) { - this._acnCircuit = acnCircuit; - notifyListeners(); - } - - // Indicated whether the conversation is archived, in which case it will - // be moved to the very bottom of the active conversations list until - // new messages appear - set isArchived(bool archived) { - this._archived = archived; - notifyListeners(); - } - - bool get isArchived => this._archived; - - set savePeerHistory(String newVal) { - this._savePeerHistory = newVal; - notifyListeners(); - } - - set nickname(String newVal) { - this._nickname = newVal; - notifyListeners(); - } - - bool get isGroup => this._isGroup; - set isGroup(bool newVal) { - this._isGroup = newVal; - notifyListeners(); - } - - bool get isBlocked => this._blocked; - - bool get isInvitation => !this._blocked && !this._accepted; - - set accepted(bool newVal) { - this._accepted = newVal; - notifyListeners(); - } - - set blocked(bool newVal) { - this._blocked = newVal; - notifyListeners(); - } - - String get status => this._status; - set status(String newVal) { - this._status = newVal; - notifyListeners(); - } - - int get unreadMessages => this._unreadMessages; - set unreadMessages(int newVal) { - // don't reset newMarker position when unreadMessages is being cleared - if (newVal > 0) { - this._newMarker = newVal; - } else { - this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2)); - } - this._unreadMessages = newVal; - notifyListeners(); - } - - int get newMarker { - if (DateTime.now().isAfter(this._newMarkerClearAt)) { - // perform heresy - this._newMarker = 0; - // no need to notifyListeners() because presumably this getter is - // being called from a renderer anyway - } - return this._newMarker; - } - - // what's a getter that sometimes sets without a setter - // that sometimes doesn't set - set newMarker(int newVal) { - // only unreadMessages++ can set newMarker = 1; - // avoids drawing a marker when the convo is already open - if (newVal >= 1) { - this._newMarker = newVal; - notifyListeners(); - } - } - - int get totalMessages => this._totalMessages; - set totalMessages(int newVal) { - this._totalMessages = newVal; - notifyListeners(); - } - - String get imagePath => this._imagePath; - set imagePath(String newVal) { - this._imagePath = newVal; - notifyListeners(); - } - - DateTime get lastMessageTime => this._lastMessageTime; - set lastMessageTime(DateTime newVal) { - this._lastMessageTime = newVal; - notifyListeners(); - } - - // we only allow callers to fetch the server - get server => this._server; - - bool isOnline() { - if (this.isGroup == true) { - // We now have an out of sync warning so we will mark these as online... - return this.status == "Authenticated" || this.status == "Synced"; - } else { - return this.status == "Authenticated"; - } - } - - GlobalKey getMessageKey(int conversation, int message) { - String index = "c: " + conversation.toString() + " m:" + message.toString(); - if (keys[index] == null) { - keys[index] = GlobalKey(); - } - GlobalKey ret = keys[index]!; - return ret; - } - - GlobalKey? getMessageKeyOrFail(int conversation, int message) { - String index = "c: " + conversation.toString() + " m:" + message.toString(); - - if (keys[index] == null) { - return null; - } - GlobalKey ret = keys[index]!; - return ret; - } - - void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { - this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); - this.totalMessages += 1; - } - - void bumpMessageCache() { - this.messageCache.insert(0, null); - this.totalMessages += 1; - } - - void ackCache(int messageID) { - this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; - notifyListeners(); - } -} diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart new file mode 100644 index 00000000..39ae8454 --- /dev/null +++ b/lib/models/appstate.dart @@ -0,0 +1,71 @@ +import 'package:flutter/widgets.dart'; + +enum ModalState { none, storageMigration } + +class AppState extends ChangeNotifier { + bool cwtchInit = false; + ModalState modalState = ModalState.none; + bool cwtchIsClosing = false; + String appError = ""; + String? _selectedProfile; + int? _selectedConversation; + int _initialScrollIndex = 0; + int _hoveredIndex = -1; + int? _selectedIndex; + bool _unreadMessagesBelow = false; + + void SetCwtchInit() { + cwtchInit = true; + notifyListeners(); + } + + void SetAppError(String error) { + appError = error; + notifyListeners(); + } + + void SetModalState(ModalState newState) { + modalState = newState; + notifyListeners(); + } + + String? get selectedProfile => _selectedProfile; + set selectedProfile(String? newVal) { + this._selectedProfile = newVal; + notifyListeners(); + } + + int? get selectedConversation => _selectedConversation; + set selectedConversation(int? newVal) { + this._selectedConversation = newVal; + notifyListeners(); + } + + int? get selectedIndex => _selectedIndex; + set selectedIndex(int? newVal) { + this._selectedIndex = newVal; + notifyListeners(); + } + + // Never use this for message lookup - can be a non-indexed value + // e.g. -1 + int get hoveredIndex => _hoveredIndex; + set hoveredIndex(int newVal) { + this._hoveredIndex = newVal; + notifyListeners(); + } + + bool get unreadMessagesBelow => _unreadMessagesBelow; + set unreadMessagesBelow(bool newVal) { + this._unreadMessagesBelow = newVal; + notifyListeners(); + } + + int get initialScrollIndex => _initialScrollIndex; + set initialScrollIndex(int newVal) { + this._initialScrollIndex = newVal; + notifyListeners(); + } + + bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; +} \ No newline at end of file diff --git a/lib/models/chatmessage.dart b/lib/models/chatmessage.dart new file mode 100644 index 00000000..f45d9e95 --- /dev/null +++ b/lib/models/chatmessage.dart @@ -0,0 +1,15 @@ +class ChatMessage { + final int o; + final String d; + + ChatMessage({required this.o, required this.d}); + + ChatMessage.fromJson(Map json) + : o = json['o'], + d = json['d']; + + Map toJson() => { + 'o': o, + 'd': d, + }; +} \ No newline at end of file diff --git a/lib/models/contact.dart b/lib/models/contact.dart new file mode 100644 index 00000000..0b840b5c --- /dev/null +++ b/lib/models/contact.dart @@ -0,0 +1,214 @@ +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; + +import 'message.dart'; +import 'messagecache.dart'; + +class ContactInfoState extends ChangeNotifier { + final String profileOnion; + final int identifier; + final String onion; + late String _nickname; + + late bool _accepted; + late bool _blocked; + late String _status; + late String _imagePath; + late String _savePeerHistory; + late int _unreadMessages = 0; + late int _totalMessages = 0; + late DateTime _lastMessageTime; + late Map> keys; + late List messageCache; + int _newMarker = 0; + DateTime _newMarkerClearAt = DateTime.now(); + + // todo: a nicer way to model contacts, groups and other "entities" + late bool _isGroup; + String? _server; + late bool _archived; + + String? _acnCircuit; + + ContactInfoState(this.profileOnion, this.identifier, this.onion, + {nickname = "", + isGroup = false, + accepted = false, + blocked = false, + status = "", + imagePath = "", + savePeerHistory = "DeleteHistoryConfirmed", + numMessages = 0, + numUnread = 0, + lastMessageTime, + server, + archived = false}) { + this._nickname = nickname; + this._isGroup = isGroup; + this._accepted = accepted; + this._blocked = blocked; + this._status = status; + this._imagePath = imagePath; + this._totalMessages = numMessages; + this._unreadMessages = numUnread; + this._savePeerHistory = savePeerHistory; + this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; + this._server = server; + this._archived = archived; + this.messageCache = List.empty(growable: true); + keys = Map>(); + } + + String get nickname => this._nickname; + + String get savePeerHistory => this._savePeerHistory; + + String? get acnCircuit => this._acnCircuit; + set acnCircuit(String? acnCircuit) { + this._acnCircuit = acnCircuit; + notifyListeners(); + } + + // Indicated whether the conversation is archived, in which case it will + // be moved to the very bottom of the active conversations list until + // new messages appear + set isArchived(bool archived) { + this._archived = archived; + notifyListeners(); + } + + bool get isArchived => this._archived; + + set savePeerHistory(String newVal) { + this._savePeerHistory = newVal; + notifyListeners(); + } + + set nickname(String newVal) { + this._nickname = newVal; + notifyListeners(); + } + + bool get isGroup => this._isGroup; + set isGroup(bool newVal) { + this._isGroup = newVal; + notifyListeners(); + } + + bool get isBlocked => this._blocked; + + bool get isInvitation => !this._blocked && !this._accepted; + + set accepted(bool newVal) { + this._accepted = newVal; + notifyListeners(); + } + + set blocked(bool newVal) { + this._blocked = newVal; + notifyListeners(); + } + + String get status => this._status; + set status(String newVal) { + this._status = newVal; + notifyListeners(); + } + + int get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { + // don't reset newMarker position when unreadMessages is being cleared + if (newVal > 0) { + this._newMarker = newVal; + } else { + this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2)); + } + this._unreadMessages = newVal; + notifyListeners(); + } + + int get newMarker { + if (DateTime.now().isAfter(this._newMarkerClearAt)) { + // perform heresy + this._newMarker = 0; + // no need to notifyListeners() because presumably this getter is + // being called from a renderer anyway + } + return this._newMarker; + } + + // what's a getter that sometimes sets without a setter + // that sometimes doesn't set + set newMarker(int newVal) { + // only unreadMessages++ can set newMarker = 1; + // avoids drawing a marker when the convo is already open + if (newVal >= 1) { + this._newMarker = newVal; + notifyListeners(); + } + } + + int get totalMessages => this._totalMessages; + set totalMessages(int newVal) { + this._totalMessages = newVal; + notifyListeners(); + } + + String get imagePath => this._imagePath; + set imagePath(String newVal) { + this._imagePath = newVal; + notifyListeners(); + } + + DateTime get lastMessageTime => this._lastMessageTime; + set lastMessageTime(DateTime newVal) { + this._lastMessageTime = newVal; + notifyListeners(); + } + + // we only allow callers to fetch the server + get server => this._server; + + bool isOnline() { + if (this.isGroup == true) { + // We now have an out of sync warning so we will mark these as online... + return this.status == "Authenticated" || this.status == "Synced"; + } else { + return this.status == "Authenticated"; + } + } + + GlobalKey getMessageKey(int conversation, int message) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); + if (keys[index] == null) { + keys[index] = GlobalKey(); + } + GlobalKey ret = keys[index]!; + return ret; + } + + GlobalKey? getMessageKeyOrFail(int conversation, int message) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); + + if (keys[index] == null) { + return null; + } + GlobalKey ret = keys[index]!; + return ret; + } + + void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { + this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); + this.totalMessages += 1; + } + + void bumpMessageCache() { + this.messageCache.insert(0, null); + this.totalMessages += 1; + } + + void ackCache(int messageID) { + this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart new file mode 100644 index 00000000..d61038a3 --- /dev/null +++ b/lib/models/contactlist.dart @@ -0,0 +1,125 @@ +import 'package:flutter/widgets.dart'; + +import 'contact.dart'; +import 'profileservers.dart'; + +class ContactListState extends ChangeNotifier { + ProfileServerListState? servers; + List _contacts = []; + String _filter = ""; + int get num => _contacts.length; + int get numFiltered => isFiltered ? filteredList().length : num; + bool get isFiltered => _filter != ""; + String get filter => _filter; + set filter(String newVal) { + _filter = newVal.toLowerCase(); + notifyListeners(); + } + + void connectServers(ProfileServerListState servers) { + this.servers = servers; + } + + List filteredList() { + if (!isFiltered) return contacts; + return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); + } + + void addAll(Iterable newContacts) { + _contacts.addAll(newContacts); + servers?.clearGroups(); + _contacts.forEach((contact) { + if (contact.isGroup) { + servers?.addGroup(contact); + } + }); + resort(); + notifyListeners(); + } + + void add(ContactInfoState newContact) { + _contacts.add(newContact); + if (newContact.isGroup) { + servers?.addGroup(newContact); + } + resort(); + notifyListeners(); + } + + void resort() { + _contacts.sort((ContactInfoState a, ContactInfoState b) { + // return -1 = a first in list + // return 1 = b first in list + + // blocked contacts last + if (a.isBlocked == true && b.isBlocked != true) return 1; + if (a.isBlocked != true && b.isBlocked == true) return -1; + // archive is next... + if (!a.isArchived && b.isArchived) return -1; + if (a.isArchived && !b.isArchived) return 1; + + // unapproved top + if (a.isInvitation && !b.isInvitation) return -1; + if (!a.isInvitation && b.isInvitation) return 1; + + // special sorting for contacts with no messages in either history + if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) { + // online contacts first + if (a.isOnline() && !b.isOnline()) return -1; + if (!a.isOnline() && b.isOnline()) return 1; + // finally resort to onion + return a.onion.toString().compareTo(b.onion.toString()); + } + // finally... most recent history first + if (a.lastMessageTime.millisecondsSinceEpoch == 0) return 1; + if (b.lastMessageTime.millisecondsSinceEpoch == 0) return -1; + return b.lastMessageTime.compareTo(a.lastMessageTime); + }); + // if(changed) { + notifyListeners(); + //} + } + + void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) { + var contact = getContact(forIdentifier); + if (contact == null) return; + + // Assert that the new time is after the current last message time AND that + // new message time is before the current time. + if (newMessageTime.isAfter(contact.lastMessageTime)) { + if (newMessageTime.isBefore(DateTime.now().toLocal())) { + contact.lastMessageTime = newMessageTime; + } else { + // Otherwise set the last message time to now... + contact.lastMessageTime = DateTime.now().toLocal(); + } + resort(); + } + } + + List get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier + + ContactInfoState? getContact(int identifier) { + int idx = _contacts.indexWhere((element) => element.identifier == identifier); + return idx >= 0 ? _contacts[idx] : null; + } + + void removeContact(int identifier) { + int idx = _contacts.indexWhere((element) => element.identifier == identifier); + if (idx >= 0) { + _contacts.removeAt(idx); + notifyListeners(); + } + } + + void removeContactByHandle(String handle) { + int idx = _contacts.indexWhere((element) => element.onion == handle); + _contacts.removeAt(idx); + notifyListeners(); + } + + ContactInfoState? findContact(String byHandle) { + int idx = _contacts.indexWhere((element) => element.onion == byHandle); + return idx >= 0 ? _contacts[idx] : null; + } +} \ No newline at end of file diff --git a/lib/models/filedownloadprogress.dart b/lib/models/filedownloadprogress.dart new file mode 100644 index 00000000..ea5c279a --- /dev/null +++ b/lib/models/filedownloadprogress.dart @@ -0,0 +1,27 @@ +class FileDownloadProgress { + int chunksDownloaded = 0; + int chunksTotal = 1; + bool complete = false; + bool gotManifest = false; + bool interrupted = false; + String? downloadedTo; + DateTime? timeStart; + DateTime? timeEnd; + + FileDownloadProgress(this.chunksTotal, this.timeStart); + double progress() { + return 1.0 * chunksDownloaded / chunksTotal; + } +} + +String prettyBytes(int bytes) { + if (bytes > 1000000000) { + return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB"; + } else if (bytes > 1000000) { + return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB"; + } else if (bytes > 1000) { + return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB"; + } else { + return bytes.toString() + " B"; + } +} \ No newline at end of file diff --git a/lib/models/message.dart b/lib/models/message.dart index dc4c5b97..91701cc8 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -4,12 +4,12 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; import 'messages/filemessage.dart'; import 'messages/invitemessage.dart'; import 'messages/malformedmessage.dart'; import 'messages/quotedmessage.dart'; import 'messages/textmessage.dart'; +import 'profile.dart'; // Define the overlays const TextMessageOverlay = 1; diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart new file mode 100644 index 00000000..8f9d5e16 --- /dev/null +++ b/lib/models/messagecache.dart @@ -0,0 +1,7 @@ +import 'message.dart'; + +class MessageCache { + final MessageMetadata metadata; + final String wrapper; + MessageCache(this.metadata, this.wrapper); +} \ No newline at end of file diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index c155ef60..c68b24e5 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../../main.dart'; -import '../../model.dart'; +import '../profile.dart'; class FileMessage extends Message { final MessageMetadata metadata; diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index 2965abc9..50a28806 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -7,7 +7,7 @@ import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -import '../../model.dart'; +import '../profile.dart'; class InviteMessage extends Message { final MessageMetadata metadata; diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 2eb54b28..7d34c1b5 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -9,7 +9,6 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../../main.dart'; -import '../../model.dart'; class QuotedMessageStructure { final String quotedHash; diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index a8d7f6af..44f99510 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -8,8 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -import '../../model.dart'; - class TextMessage extends Message { final MessageMetadata metadata; final String content; diff --git a/lib/models/profile.dart b/lib/models/profile.dart new file mode 100644 index 00000000..a4406b69 --- /dev/null +++ b/lib/models/profile.dart @@ -0,0 +1,272 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; + +import 'contact.dart'; +import 'contactlist.dart'; +import 'filedownloadprogress.dart'; +import 'profileservers.dart'; + +class ProfileInfoState extends ChangeNotifier { + ProfileServerListState _servers = ProfileServerListState(); + ContactListState _contacts = ContactListState(); + final String onion; + String _nickname = ""; + String _imagePath = ""; + int _unreadMessages = 0; + bool _online = false; + Map _downloads = Map(); + + // assume profiles are encrypted...this will be set to false + // in the constructor if the profile is encrypted with the defacto password. + bool _encrypted = true; + + ProfileInfoState({ + required this.onion, + nickname = "", + imagePath = "", + unreadMessages = 0, + contactsJson = "", + serversJson = "", + online = false, + encrypted = true, + }) { + this._nickname = nickname; + this._imagePath = imagePath; + this._unreadMessages = unreadMessages; + this._online = online; + this._encrypted = encrypted; + + _contacts.connectServers(this._servers); + + if (contactsJson != null && contactsJson != "" && contactsJson != "null") { + this.replaceServers(serversJson); + + List contacts = jsonDecode(contactsJson); + this._contacts.addAll(contacts.map((contact) { + return ContactInfoState(this.onion, contact["identifier"], contact["onion"], + nickname: contact["name"], + status: contact["status"], + imagePath: contact["picture"], + accepted: contact["accepted"], + blocked: contact["blocked"], + savePeerHistory: contact["saveConversationHistory"], + numMessages: contact["numMessages"], + numUnread: contact["numUnread"], + isGroup: contact["isGroup"], + server: contact["groupServer"], + archived: contact["isArchived"] == true, + lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]))); + })); + + // dummy set to invoke sort-on-load + if (this._contacts.num > 0) { + this._contacts.updateLastMessageTime(this._contacts.contacts.first.identifier, this._contacts.contacts.first.lastMessageTime); + } + } + } + + // Parse out the server list json into our server info state struct... + void replaceServers(String serversJson) { + if (serversJson != "" && serversJson != "null") { + List servers = jsonDecode(serversJson); + this._servers.replace(servers.map((server) { + // TODO Keys... + return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]); + })); + + this._contacts.contacts.forEach((contact) { + if (contact.isGroup) { + _servers.addGroup(contact); + } + }); + + notifyListeners(); + } + } + + // + void updateServerStatusCache(String server, String status) { + this._servers.updateServerState(server, status); + notifyListeners(); + } + + // Getters and Setters for Online Status + bool get isOnline => this._online; + set isOnline(bool newValue) { + this._online = newValue; + notifyListeners(); + } + + // Check encrypted status for profile info screen + bool get isEncrypted => this._encrypted; + + String get nickname => this._nickname; + set nickname(String newValue) { + this._nickname = newValue; + notifyListeners(); + } + + String get imagePath => this._imagePath; + set imagePath(String newVal) { + this._imagePath = newVal; + notifyListeners(); + } + + int get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { + this._unreadMessages = newVal; + notifyListeners(); + } + + // Remove a contact from a list. Currently only used when rejecting a group invitation. + // Eventually will also be used for other removals. + void removeContact(String handle) { + this.contactList.removeContactByHandle(handle); + notifyListeners(); + } + + ContactListState get contactList => this._contacts; + ProfileServerListState get serverList => this._servers; + + @override + void dispose() { + super.dispose(); + print("profileinfostate.dispose()"); + } + + void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) { + this._nickname = name; + this._imagePath = picture; + this._online = online; + this.replaceServers(serverJson); + + if (contactsJson != null && contactsJson != "" && contactsJson != "null") { + List contacts = jsonDecode(contactsJson); + contacts.forEach((contact) { + var profileContact = this._contacts.getContact(contact["identifier"]); + if (profileContact != null) { + profileContact.status = contact["status"]; + profileContact.totalMessages = contact["numMessages"]; + profileContact.unreadMessages = contact["numUnread"]; + profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])); + } else { + this._contacts.add(ContactInfoState( + this.onion, + contact["identifier"], + contact["onion"], + nickname: contact["name"], + status: contact["status"], + imagePath: contact["picture"], + accepted: contact["accepted"], + blocked: contact["blocked"], + savePeerHistory: contact["saveConversationHistory"], + numMessages: contact["numMessages"], + numUnread: contact["numUnread"], + isGroup: contact["isGroup"], + server: contact["groupServer"], + lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), + )); + } + }); + } + this._contacts.resort(); + } + + void downloadInit(String fileKey, int numChunks) { + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); + } + + void downloadUpdate(String fileKey, int progress, int numChunks) { + if (!downloadActive(fileKey)) { + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); + if (progress < 0) { + this._downloads[fileKey]!.interrupted = true; + } + } else { + if (this._downloads[fileKey]!.interrupted) { + this._downloads[fileKey]!.interrupted = false; + } + this._downloads[fileKey]!.chunksDownloaded = progress; + this._downloads[fileKey]!.chunksTotal = numChunks; + } + notifyListeners(); + } + + void downloadMarkManifest(String fileKey) { + if (!downloadActive(fileKey)) { + this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); + } + this._downloads[fileKey]!.gotManifest = true; + notifyListeners(); + } + + void downloadMarkFinished(String fileKey, String finalPath) { + if (!downloadActive(fileKey)) { + // happens as a result of a CheckDownloadStatus call, + // invoked from a historical (timeline) download message + // so setting numChunks correctly shouldn't matter + this.downloadInit(fileKey, 1); + } + // only update if different + if (!this._downloads[fileKey]!.complete) { + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.downloadedTo = finalPath; + this._downloads[fileKey]!.complete = true; + notifyListeners(); + } + } + + bool downloadKnown(String fileKey) { + return this._downloads.containsKey(fileKey); + } + + bool downloadActive(String fileKey) { + return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted; + } + + bool downloadGotManifest(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest; + } + + bool downloadComplete(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; + } + + bool downloadInterrupted(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted; + } + + void downloadMarkResumed(String fileKey) { + if (this._downloads.containsKey(fileKey)) { + this._downloads[fileKey]!.interrupted = false; + } + } + + double downloadProgress(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; + } + + // used for loading interrupted download info; use downloadMarkFinished for successful downloads + void downloadSetPath(String fileKey, String path) { + if (this._downloads.containsKey(fileKey)) { + this._downloads[fileKey]!.downloadedTo = path; + } + } + + String? downloadFinalPath(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; + } + + String downloadSpeed(String fileKey) { + if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) { + return "0 B/s"; + } + var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; + var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds; + if (seconds == 0) { + return "0 B/s"; + } + return prettyBytes((bytes / seconds).round()) + "/s"; + } +} \ No newline at end of file diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart new file mode 100644 index 00000000..34eb2526 --- /dev/null +++ b/lib/models/profilelist.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart'; + +import 'profile.dart'; + +class ProfileListState extends ChangeNotifier { + List _profiles = []; + int get num => _profiles.length; + + void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { + var idx = _profiles.indexWhere((element) => element.onion == onion); + if (idx == -1) { + _profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted)); + } else { + _profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online); + } + notifyListeners(); + } + + List get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier + + ProfileInfoState? getProfile(String onion) { + int idx = _profiles.indexWhere((element) => element.onion == onion); + return idx >= 0 ? _profiles[idx] : null; + } + + void delete(String onion) { + _profiles.removeWhere((element) => element.onion == onion); + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index 4b868b95..9dd7af49 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -1,6 +1,7 @@ -import 'package:cwtch/model.dart'; import 'package:flutter/material.dart'; +import 'contact.dart'; + class ProfileServerListState extends ChangeNotifier { List _servers = []; diff --git a/lib/views/addcontactview.dart b/lib/views/addcontactview.dart index f8acf20c..2d58c43f 100644 --- a/lib/views/addcontactview.dart +++ b/lib/views/addcontactview.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cwtch/errorHandler.dart'; @@ -13,7 +14,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; /// Add Contact View is the one-stop shop for adding public keys to a Profiles contact list. /// We support both Peers and Groups (experiment-pending). diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 1961ac9e..7d084f5a 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -4,9 +4,9 @@ import 'dart:math'; import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch/cwtch.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cwtch/model.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:cwtch/widgets/passwordfield.dart'; diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 09cc7d5b..4e7b6ade 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -1,7 +1,10 @@ import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/contactlist.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/views/profileserversview.dart'; import 'package:flutter/material.dart'; -import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/widgets/contactrow.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/textfield.dart'; @@ -10,7 +13,6 @@ import 'package:provider/provider.dart'; import '../main.dart'; import '../settings.dart'; import 'addcontactview.dart'; -import '../model.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'messageview.dart'; diff --git a/lib/views/doublecolview.dart b/lib/views/doublecolview.dart index 4650c0ed..60714fcf 100644 --- a/lib/views/doublecolview.dart +++ b/lib/views/doublecolview.dart @@ -1,8 +1,10 @@ +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; import 'contactsview.dart'; import 'messageview.dart'; diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 6407c4ef..18855e7e 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -1,6 +1,8 @@ import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/services.dart'; -import 'package:cwtch/model.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 5d21ab8e..877addb7 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -3,8 +3,12 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/chatmessage.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; @@ -22,7 +26,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:path/path.dart' show basename; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; import '../widgets/messagelist.dart'; import 'groupsettingsview.dart'; diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 6606312d..0ab762ee 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -1,8 +1,9 @@ import 'dart:convert'; import 'dart:ui'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:flutter/services.dart'; -import 'package:cwtch/model.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index 97a7c21b..cbbc3a68 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/profile.dart'; +import 'package:cwtch/models/profilelist.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/settings.dart'; import 'package:cwtch/views/torstatusview.dart'; @@ -13,7 +16,6 @@ import 'package:cwtch/widgets/profilerow.dart'; import 'package:provider/provider.dart'; import '../config.dart'; import '../main.dart'; -import '../model.dart'; import '../torstatus.dart'; import 'addeditprofileview.dart'; import 'globalsettingsview.dart'; diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart index 1fd029b4..6a4d3ebd 100644 --- a/lib/views/profileserversview.dart +++ b/lib/views/profileserversview.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/widgets/remoteserverrow.dart'; @@ -7,7 +8,6 @@ import 'package:provider/provider.dart'; import '../cwtch_icons_icons.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; class ProfileServersView extends StatefulWidget { diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart index 18bcdd04..46ac8567 100644 --- a/lib/views/remoteserverview.dart +++ b/lib/views/remoteserverview.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:cwtch/cwtch/cwtch.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; @@ -17,7 +19,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../errorHandler.dart'; import '../main.dart'; import '../config.dart'; -import '../model.dart'; /// Pane to add or edit a server class RemoteServerView extends StatefulWidget { diff --git a/lib/views/splashView.dart b/lib/views/splashView.dart index 9e68b2f8..fe24af8f 100644 --- a/lib/views/splashView.dart +++ b/lib/views/splashView.dart @@ -1,9 +1,9 @@ +import 'package:cwtch/models/appstate.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../model.dart'; import '../settings.dart'; class SplashView extends StatefulWidget { diff --git a/lib/widgets/DropdownContacts.dart b/lib/widgets/DropdownContacts.dart index aed9c4ac..6c70ecaa 100644 --- a/lib/widgets/DropdownContacts.dart +++ b/lib/widgets/DropdownContacts.dart @@ -1,7 +1,8 @@ +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../model.dart'; bool noFilter(ContactInfoState peer) { return true; diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index ee1ba242..53dfe32c 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -1,5 +1,8 @@ import 'dart:io'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/views/contactsview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; @@ -8,7 +11,6 @@ import 'package:cwtch/widgets/profileimage.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; import 'package:intl/intl.dart'; diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index ea937303..1d6c347c 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -1,14 +1,16 @@ import 'dart:io'; import 'package:cwtch/config.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/filedownloadprogress.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 2839b550..c03cb078 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -2,12 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 948351cd..d764d1e6 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -1,13 +1,14 @@ import 'dart:io'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/third_party/linkify/flutter_linkify.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 57009768..aa29baf2 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -1,11 +1,13 @@ +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../model.dart'; import '../settings.dart'; class MessageList extends StatefulWidget { diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 2cc51647..8661305d 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,7 +1,10 @@ import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/views/contactsview.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/widgets/profileimage.dart'; @@ -10,7 +13,6 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; class MessageRow extends StatefulWidget { diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index 7fccb90f..6d2c8e8e 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -1,3 +1,6 @@ +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contactlist.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:cwtch/views/addeditprofileview.dart'; @@ -9,7 +12,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../errorHandler.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; class ProfileRow extends StatefulWidget { diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index e2af135e..31e8a80a 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -1,9 +1,10 @@ +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import '../settings.dart'; diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart index e7b2417a..c35b0e9a 100644 --- a/lib/widgets/remoteserverrow.dart +++ b/lib/widgets/remoteserverrow.dart @@ -1,4 +1,5 @@ import 'package:cwtch/main.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/views/addeditservers.dart'; @@ -11,7 +12,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../cwtch_icons_icons.dart'; import '../errorHandler.dart'; -import '../model.dart'; import '../settings.dart'; class RemoteServerRow extends StatefulWidget { diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index cb5790df..86f1c821 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -9,7 +9,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../cwtch_icons_icons.dart'; import '../errorHandler.dart'; -import '../model.dart'; import '../settings.dart'; class ServerRow extends StatefulWidget { From 52e22c085f92b848a1bf3771a688a04bfb56e16d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 19 Jan 2022 13:29:13 -0800 Subject: [PATCH 22/47] Update lcg --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index b8bbdfcc..c6c568fa 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-18-16-33-v1.5.4-8-g2aea700 \ No newline at end of file +2022-01-19-16-16-v1.5.4-11-g84d451f \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 2ea2c4de..8c390298 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-18-21-29-v1.5.4-8-g2aea700 \ No newline at end of file +2022-01-19-21-15-v1.5.4-11-g84d451f \ No newline at end of file From 19f73eb075ae459bccc1f0110114c0a1cb66d79a Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 19 Jan 2022 13:58:52 -0800 Subject: [PATCH 23/47] Formatting --- lib/models/appstate.dart | 2 +- lib/models/chatmessage.dart | 8 +++---- lib/models/contact.dart | 24 ++++++++++----------- lib/models/contactlist.dart | 2 +- lib/models/filedownloadprogress.dart | 2 +- lib/models/messagecache.dart | 2 +- lib/models/profile.dart | 32 ++++++++++++++-------------- lib/models/profilelist.dart | 2 +- lib/widgets/DropdownContacts.dart | 1 - 9 files changed, 37 insertions(+), 38 deletions(-) diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 39ae8454..8386b8dd 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -68,4 +68,4 @@ class AppState extends ChangeNotifier { } bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; -} \ No newline at end of file +} diff --git a/lib/models/chatmessage.dart b/lib/models/chatmessage.dart index f45d9e95..348172e2 100644 --- a/lib/models/chatmessage.dart +++ b/lib/models/chatmessage.dart @@ -9,7 +9,7 @@ class ChatMessage { d = json['d']; Map toJson() => { - 'o': o, - 'd': d, - }; -} \ No newline at end of file + 'o': o, + 'd': d, + }; +} diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 0b840b5c..55076712 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -32,17 +32,17 @@ class ContactInfoState extends ChangeNotifier { ContactInfoState(this.profileOnion, this.identifier, this.onion, {nickname = "", - isGroup = false, - accepted = false, - blocked = false, - status = "", - imagePath = "", - savePeerHistory = "DeleteHistoryConfirmed", - numMessages = 0, - numUnread = 0, - lastMessageTime, - server, - archived = false}) { + isGroup = false, + accepted = false, + blocked = false, + status = "", + imagePath = "", + savePeerHistory = "DeleteHistoryConfirmed", + numMessages = 0, + numUnread = 0, + lastMessageTime, + server, + archived = false}) { this._nickname = nickname; this._isGroup = isGroup; this._accepted = accepted; @@ -211,4 +211,4 @@ class ContactInfoState extends ChangeNotifier { this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; notifyListeners(); } -} \ No newline at end of file +} diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart index d61038a3..a00121d9 100644 --- a/lib/models/contactlist.dart +++ b/lib/models/contactlist.dart @@ -122,4 +122,4 @@ class ContactListState extends ChangeNotifier { int idx = _contacts.indexWhere((element) => element.onion == byHandle); return idx >= 0 ? _contacts[idx] : null; } -} \ No newline at end of file +} diff --git a/lib/models/filedownloadprogress.dart b/lib/models/filedownloadprogress.dart index ea5c279a..0a4394af 100644 --- a/lib/models/filedownloadprogress.dart +++ b/lib/models/filedownloadprogress.dart @@ -24,4 +24,4 @@ String prettyBytes(int bytes) { } else { return bytes.toString() + " B"; } -} \ No newline at end of file +} diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index 8f9d5e16..f7fb1085 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -4,4 +4,4 @@ class MessageCache { final MessageMetadata metadata; final String wrapper; MessageCache(this.metadata, this.wrapper); -} \ No newline at end of file +} diff --git a/lib/models/profile.dart b/lib/models/profile.dart index a4406b69..a9191653 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -152,21 +152,21 @@ class ProfileInfoState extends ChangeNotifier { profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])); } else { this._contacts.add(ContactInfoState( - this.onion, - contact["identifier"], - contact["onion"], - nickname: contact["name"], - status: contact["status"], - imagePath: contact["picture"], - accepted: contact["accepted"], - blocked: contact["blocked"], - savePeerHistory: contact["saveConversationHistory"], - numMessages: contact["numMessages"], - numUnread: contact["numUnread"], - isGroup: contact["isGroup"], - server: contact["groupServer"], - lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), - )); + this.onion, + contact["identifier"], + contact["onion"], + nickname: contact["name"], + status: contact["status"], + imagePath: contact["picture"], + accepted: contact["accepted"], + blocked: contact["blocked"], + savePeerHistory: contact["saveConversationHistory"], + numMessages: contact["numMessages"], + numUnread: contact["numUnread"], + isGroup: contact["isGroup"], + server: contact["groupServer"], + lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), + )); } }); } @@ -269,4 +269,4 @@ class ProfileInfoState extends ChangeNotifier { } return prettyBytes((bytes / seconds).round()) + "/s"; } -} \ No newline at end of file +} diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart index 34eb2526..1e0eb3e2 100644 --- a/lib/models/profilelist.dart +++ b/lib/models/profilelist.dart @@ -27,4 +27,4 @@ class ProfileListState extends ChangeNotifier { _profiles.removeWhere((element) => element.onion == onion); notifyListeners(); } -} \ No newline at end of file +} diff --git a/lib/widgets/DropdownContacts.dart b/lib/widgets/DropdownContacts.dart index 6c70ecaa..2247f9b4 100644 --- a/lib/widgets/DropdownContacts.dart +++ b/lib/widgets/DropdownContacts.dart @@ -3,7 +3,6 @@ import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; - bool noFilter(ContactInfoState peer) { return true; } From 2495814869ea763706f1c1aee009c7dfa5235e4a Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 19 Jan 2022 14:07:45 -0800 Subject: [PATCH 24/47] Fixup Widths on Small Screens --- lib/views/globalsettingsview.dart | 2 +- lib/views/torstatusview.dart | 4 ++-- lib/widgets/folderpicker.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 78e3a829..1aab16d1 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -133,7 +133,7 @@ class _GlobalSettingsViewState extends State { ), leading: Icon(Icons.table_chart, color: settings.current().mainTextColor), trailing: Container( - width: 200.0, + width: MediaQuery.of(context).size.width / 4, child: DropdownButton( isExpanded: true, value: settings.uiColumnModeLandscape.toString(), diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index ed2ac999..5abdee60 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -108,7 +108,7 @@ class _TorStatusView extends State { subtitle: Text(AppLocalizations.of(context)!.torSettingsCustomSocksPortDescription), leading: Icon(CwtchIcons.swap_horiz_24px, color: settings.current().mainTextColor), trailing: Container( - width: 300, + width: MediaQuery.of(context).size.width / 4, child: CwtchTextField( number: true, controller: torSocksPortController, @@ -139,7 +139,7 @@ class _TorStatusView extends State { subtitle: Text(AppLocalizations.of(context)!.torSettingsCustomControlPortDescription), leading: Icon(CwtchIcons.swap_horiz_24px, color: settings.current().mainTextColor), trailing: Container( - width: 300, + width: MediaQuery.of(context).size.width / 4, child: CwtchTextField( number: true, controller: torControlPortController, diff --git a/lib/widgets/folderpicker.dart b/lib/widgets/folderpicker.dart index 9423aec4..d01707ec 100644 --- a/lib/widgets/folderpicker.dart +++ b/lib/widgets/folderpicker.dart @@ -37,7 +37,7 @@ class _CwtchFolderPickerState extends State { title: Text(widget.label), subtitle: Text(widget.description), trailing: Container( - width: 200, + width: MediaQuery.of(context).size.width / 4, child: CwtchButtonTextField( controller: ctrlrVal, readonly: Platform.isAndroid, From e7b9f5bb9688b394c97d8b08186cbc7dac8a185f Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 18 Jan 2022 16:26:52 -0500 Subject: [PATCH 25/47] move all classes in model.dart to their own models/X.dart --- lib/models/appstate.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 8386b8dd..39ae8454 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -68,4 +68,4 @@ class AppState extends ChangeNotifier { } bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; -} +} \ No newline at end of file From d5cb37ed9cb4c27941c7602db526dffc47689622 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 18 Jan 2022 18:31:10 -0500 Subject: [PATCH 26/47] stub of new cache --- lib/models/contact.dart | 3 +-- lib/models/messagecache.dart | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 55076712..5f632df7 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -19,7 +19,6 @@ class ContactInfoState extends ChangeNotifier { late int _totalMessages = 0; late DateTime _lastMessageTime; late Map> keys; - late List messageCache; int _newMarker = 0; DateTime _newMarkerClearAt = DateTime.now(); @@ -198,7 +197,7 @@ class ContactInfoState extends ChangeNotifier { } void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { - this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); + this.messageCache.insert(0, MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); this.totalMessages += 1; } diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index f7fb1085..166d2d1f 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -1,7 +1,32 @@ import 'message.dart'; -class MessageCache { +class MessageInfo { final MessageMetadata metadata; final String wrapper; - MessageCache(this.metadata, this.wrapper); + MessageInfo(this.metadata, this.wrapper); } + +class MessageCache { + late Map cache; + late List cacheByIndex; + + MessageCache() { + this.cache = {}; + this.cacheByIndex = List.empty(growable: true); + } + + + void addNew(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { + this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data); + this.cacheByIndex.insert(0, messageID); + } + + void bumpMessageCache() { + this.messageCache.insert(0, null); + this.totalMessages += 1; + } + + void ackCache(int messageID) { + cache[messageID]?.metadata.ackd = true; + } +} \ No newline at end of file From 793b6e2e1ad61b2a43adb192c66161f7b7766aff Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Thu, 20 Jan 2022 09:13:54 -0500 Subject: [PATCH 27/47] message cache expansion: stores all messages fetched, indexed by hash and id where possible --- lib/cwtch/cwtchNotifier.dart | 36 ++----- lib/models/contact.dart | 28 +++-- lib/models/contactlist.dart | 7 +- lib/models/message.dart | 143 +++++++++++++++++++++---- lib/models/messagecache.dart | 40 ++++++- lib/models/messages/quotedmessage.dart | 43 +------- lib/models/profile.dart | 1 - lib/views/messageview.dart | 7 +- lib/widgets/messagelist.dart | 2 +- lib/widgets/messagerow.dart | 4 +- 10 files changed, 202 insertions(+), 109 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index ef270f90..74f395ab 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -143,25 +143,10 @@ class CwtchNotifier { var senderHandle = data['RemotePeer']; var senderImage = data['Picture']; var isAuto = data['Auto'] == "true"; + String? contenthash = data['ContentHash']; + var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; - // We might not have received a contact created for this contact yet... - // In that case the **next** event we receive will actually update these values... - if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier) != null) { - if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++; - } else { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; - } - profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, DateTime.now()); - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"]); - - // We only ever see messages from authenticated peers. - // If the contact is marked as offline then override this - can happen when the contact is removed from the front - // end during syncing. - if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.isOnline() == false) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.status = "Authenticated"; - } - } + profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedConversation, ); break; case "PeerAcknowledgement": @@ -200,18 +185,12 @@ class CwtchNotifier { var timestampSent = DateTime.tryParse(data['TimestampSent'])!; var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.totalMessages; var isAuto = data['Auto'] == "true"; + String? contenthash = data['ContentHash']; + var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; + // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.updateMessageCache(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"]); - - //if not currently open - if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != identifier) { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.unreadMessages++; - } else { - profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier)!.newMarker++; - } - // TODO: There are 2 timestamps associated with a new group message - time sent and time received. // Sent refers to the time a profile alleges they sent a message // Received refers to the time we actually saw the message from the server @@ -222,7 +201,8 @@ class CwtchNotifier { // For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts // and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time` // and `local now`. - profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(identifier, timestampSent.toLocal()); + profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage(identifier, idx, timestampSent, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedConversation); + notificationManager.notify("New Message From Group!"); } } else { diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 5f632df7..3793fb91 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -21,6 +21,8 @@ class ContactInfoState extends ChangeNotifier { late Map> keys; int _newMarker = 0; DateTime _newMarkerClearAt = DateTime.now(); + //late List messageCache; + late MessageCache messageCache; // todo: a nicer way to model contacts, groups and other "entities" late bool _isGroup; @@ -54,7 +56,8 @@ class ContactInfoState extends ChangeNotifier { this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._server = server; this._archived = archived; - this.messageCache = List.empty(growable: true); + //this.messageCache = List.empty(growable: true); + this.messageCache = new MessageCache(); keys = Map>(); } @@ -196,18 +199,31 @@ class ContactInfoState extends ChangeNotifier { return ret; } - void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { - this.messageCache.insert(0, MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); + void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) { + if (!selectedConversation) { + unreadMessages++; + } else { + newMarker++; + } + + this.messageCache.addNew(profileOnion, identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash); this.totalMessages += 1; + + // We only ever see messages from authenticated peers. + // If the contact is marked as offline then override this - can happen when the contact is removed from the front + // end during syncing. + if (isOnline() == false) { + status = "Authenticated"; + } } void bumpMessageCache() { - this.messageCache.insert(0, null); + this.messageCache.bumpMessageCache(); this.totalMessages += 1; } void ackCache(int messageID) { - this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; + this.messageCache.ackCache(messageID); notifyListeners(); } -} +} \ No newline at end of file diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart index a00121d9..d54dfdf3 100644 --- a/lib/models/contactlist.dart +++ b/lib/models/contactlist.dart @@ -122,4 +122,9 @@ class ContactListState extends ChangeNotifier { int idx = _contacts.indexWhere((element) => element.onion == byHandle); return idx >= 0 ? _contacts[idx] : null; } -} + + void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) { + getContact(identifier)?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation); + updateLastMessageTime(identifier, DateTime.now()); + } +} \ No newline at end of file diff --git a/lib/models/message.dart b/lib/models/message.dart index 91701cc8..82fa7cd2 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -1,9 +1,11 @@ import 'dart:convert'; import 'package:cwtch/config.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../main.dart'; +import 'messagecache.dart'; import 'messages/filemessage.dart'; import 'messages/invitemessage.dart'; import 'messages/malformedmessage.dart'; @@ -28,7 +30,9 @@ const GroupConversationHandleLength = 32; abstract class Message { MessageMetadata getMetadata(); + Widget getWidget(BuildContext context, Key key); + Widget getPreviewWidget(BuildContext context); } @@ -57,48 +61,108 @@ Message compileOverlay(MessageMetadata metadata, String messageData) { } } -Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, int index, {bool byID = false}) { +Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, + {bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { + var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", {}, false, true, false); + if (!byIndex && !byID && !byHash) { + EnvironmentConfig.debugLog("Error calling messageHandler: one of byIndex, byID, byHash must be set"); + return Future.value(MalformedMessage(malformedMetadata)); + } + if ((byID && id == null) || (byIndex && index == null) || (byHash && hash == null)) { + EnvironmentConfig.debugLog("Error calling messageHandler: byType needs corresponding value and it was not set"); + return Future.value(MalformedMessage(malformedMetadata)); + } + + // Hit cache + MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); + if (messageInfo != null) { + return Future.value(compileOverlay(messageInfo.metadata, messageInfo.wrapper)); + } + + // Fetch and Cache + var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); + return messageInfoFuture.then( (MessageInfo? messageInfo) { + if (messageInfo != null) { + return compileOverlay(messageInfo.metadata, messageInfo.wrapper); + } else { + return MalformedMessage(malformedMetadata); + } + }); +} + +MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, +{bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { + // Hit cache try { var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; - if (cache != null && cache.length > index) { - if (cache[index] != null) { - return Future.value(compileOverlay(cache[index]!.metadata, cache[index]!.wrapper)); + if (cache != null) { + MessageInfo? messageInfo = null; + if (byID) { + messageInfo = cache.getById(id!); + } else if (byHash) { + messageInfo = cache.getByContentHash(hash!); + } else { + messageInfo = cache.getByIndex(index!); + } + if (messageInfo != null) { + return messageInfo; } } } catch (e) { + EnvironmentConfig.debugLog("message handler exception on get from cache: $e"); // provider check failed...make an expensive call... } + return null; +} +Future fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, +{bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { +// Load and cache try { Future rawMessageEnvelopeFuture; if (byID) { - rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, index); + rawMessageEnvelopeFuture = Provider + .of(context, listen: false) + .cwtch + .GetMessageByID(profileOnion, conversationIdentifier, id!); + } else if (byHash) { + rawMessageEnvelopeFuture = Provider + .of(context, listen: false) + .cwtch + .GetMessageByContentHash(profileOnion, conversationIdentifier, hash!); } else { - rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index); + rawMessageEnvelopeFuture = Provider + .of(context, listen: false) + .cwtch + .GetMessage(profileOnion, conversationIdentifier, index!); } return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - var metadata = MessageMetadata(profileOnion, conversationIdentifier, index, DateTime.now(), "", "", "", {}, false, true, false); try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); - // There are 2 conditions in which this error condition can be met: - // 1. The application == nil, in which case this instance of the UI is already - // broken beyond repair, and will either be replaced by a new version, or requires a complete - // restart. - // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. - // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the - // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one - // will find itself delayed. - // The second case is recoverable by tail-recursing this future. +// There are 2 conditions in which this error condition can be met: +// 1. The application == nil, in which case this instance of the UI is already +// broken beyond repair, and will either be replaced by a new version, or requires a complete +// restart. +// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. +// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the +// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one +// will find itself delayed. +// The second case is recoverable by tail-recursing this future. if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, conversationIdentifier, -1, byID: byID).then((value) => value); + return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, + index: index, + byID: byID, + id: id, + byHash: byHash, + hash: hash).then((value) => value); }); } - // Construct the initial metadata +// Construct the initial metadata var messageID = messageWrapper['ID']; var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; var senderHandle = messageWrapper['PeerID']; @@ -107,16 +171,47 @@ Future messageHandler(BuildContext context, String profileOnion, int co var ackd = messageWrapper['Acknowledged']; var error = messageWrapper['Error'] != null; var signature = messageWrapper['Signature']; - metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false); + var contenthash = messageWrapper['ContentHash']; + var localIndex = messageWrapper['LocalIndex']; + var metadata = MessageMetadata( + profileOnion, + conversationIdentifier, + messageID, + timestamp, + senderHandle, + senderImage, + signature, + attributes, + ackd, + error, + false); + var messageInfo = new MessageInfo(metadata, messageWrapper['Message']); - return compileOverlay(metadata, messageWrapper['Message']); + var cache = Provider + .of(context, listen: false) + .contactList + .getContact(conversationIdentifier) + ?.messageCache; + + if (cache != null) { + if (byID) { + cache.addUnindexed(messageInfo, contenthash); + } else if (byHash) { + cache.addUnindexed(messageInfo, contenthash); + } else { + cache.add(messageInfo, index!, contenthash); + } + } + + return messageInfo; } catch (e) { - EnvironmentConfig.debugLog("an error! " + e.toString()); - return MalformedMessage(metadata); + EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString()); + return null; } }); } catch (e) { - return Future.value(MalformedMessage(MessageMetadata(profileOnion, conversationIdentifier, -1, DateTime.now(), "", "", "", {}, false, true, false))); + EnvironmentConfig.debugLog("message handler exeption on get message: $e"); + return Future.value(null); } } @@ -139,12 +234,14 @@ class MessageMetadata extends ChangeNotifier { dynamic get attributes => this._attributes; bool get ackd => this._ackd; + set ackd(bool newVal) { this._ackd = newVal; notifyListeners(); } bool get error => this._error; + set error(bool newVal) { this._error = newVal; notifyListeners(); diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index 166d2d1f..3c11e526 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -9,21 +9,51 @@ class MessageInfo { class MessageCache { late Map cache; late List cacheByIndex; + late Map cacheByHash; MessageCache() { - this.cache = {}; - this.cacheByIndex = List.empty(growable: true); + cache = {}; + cacheByIndex = List.empty(growable: true); + cacheByHash = {}; } + int get indexedLength => cacheByIndex.length; - void addNew(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { + MessageInfo? getById(int id) => cache[id]; + MessageInfo? getByIndex(int index) { + if (index >= cacheByIndex.length) { + return null; + } + return cache[cacheByIndex[index]]; + } + MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]]; + + void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash) { this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data); this.cacheByIndex.insert(0, messageID); + if (contenthash != null && contenthash != "") { + this.cacheByHash[contenthash] = messageID; + } } + void add(MessageInfo messageInfo, int index, String? contenthash) { + this.cache[messageInfo.metadata.messageID] = messageInfo; + this.cacheByIndex.insert(index, messageInfo.metadata.messageID); + if (contenthash != null && contenthash != "") { + this.cacheByHash[contenthash] = messageInfo.metadata.messageID; + } + } + + void addUnindexed(MessageInfo messageInfo, String? contenthash) { + this.cache[messageInfo.metadata.messageID] = messageInfo; + if (contenthash != null && contenthash != "") { + this.cacheByHash[contenthash] = messageInfo.metadata.messageID; + } + } + + // TODO inserting nulls travel down list causing fails for all void bumpMessageCache() { - this.messageCache.insert(0, null); - this.totalMessages += 1; + this.cacheByIndex.insert(0, null); } void ackCache(int messageID) { diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 7d34c1b5..68615f18 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -9,6 +9,8 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../../main.dart'; +import '../messagecache.dart'; +import '../profile.dart'; class QuotedMessageStructure { final String quotedHash; @@ -21,22 +23,6 @@ class QuotedMessageStructure { }; } -class LocallyIndexedMessage { - final dynamic message; - final int index; - - LocallyIndexedMessage(this.message, this.index); - - LocallyIndexedMessage.fromJson(Map json) - : message = json['Message'], - index = json['LocalIndex']; - - Map toJson() => { - 'Message': message, - 'LocalIndex': index, - }; -} - class QuotedMessage extends Message { final MessageMetadata metadata; final String content; @@ -70,34 +56,11 @@ class QuotedMessage extends Message { return MalformedBubble(); } - var quotedMessagePotentials = Provider.of(context).cwtch.GetMessageByContentHash(metadata.profileOnion, metadata.conversationIdentifier, message["quotedHash"]); - Future quotedMessage = quotedMessagePotentials.then((matchingMessages) { - if (matchingMessages == "[]") { - return null; - } - // reverse order the messages from newest to oldest and return the - // first matching message where it's index is less than the index of this - // message - try { - var list = (jsonDecode(matchingMessages) as List).map((data) => LocallyIndexedMessage.fromJson(data)).toList(); - LocallyIndexedMessage candidate = list.reversed.first; - return candidate; - } catch (e) { - // Malformed Message will be returned... - return null; - } - }); - return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { return MessageRow( - QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) { - if (localIndex != null) { - return messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, localIndex.index); - } - return MalformedMessage(this.metadata); - })), + QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, byHash: true, hash: message["quotedHash"])), key: key); }); } catch (e) { diff --git a/lib/models/profile.dart b/lib/models/profile.dart index a9191653..ddca5aba 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -132,7 +132,6 @@ class ProfileInfoState extends ChangeNotifier { @override void dispose() { super.dispose(); - print("profileinfostate.dispose()"); } void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) { diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 877addb7..e2a22ba6 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -225,7 +225,10 @@ class _MessageViewState extends State { ctrlrCompose.clear(); focusNode.requestFocus(); Future.delayed(const Duration(milliseconds: 80), () { - Provider.of(context, listen: false).contactList.getContact(Provider.of(context, listen: false).identifier)?.bumpMessageCache(); + var profile = Provider.of(context, listen: false).profileOnion; + var identifier = Provider.of(context, listen: false).identifier; + //Provider.of(context, listen: false).contactList.getContact(Provider.of(context, listen: false).identifier)?.bumpMessageCache(); + fetchAndCacheMessageInfo(context, profile, identifier, byIndex: true, index: 0); Provider.of(context, listen: false).newMarker++; // Resort the contact list... Provider.of(context, listen: false).contactList.updateLastMessageTime(Provider.of(context, listen: false).identifier, DateTime.now()); @@ -282,7 +285,7 @@ class _MessageViewState extends State { if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { var quoted = FutureBuilder( future: - messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, Provider.of(context).selectedIndex!, byID: true), + messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, id: Provider.of(context).selectedIndex!, byID: true), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data! as Message; diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index aa29baf2..4756381d 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -83,7 +83,7 @@ class _MessageListState extends State { var messageIndex = index; return FutureBuilder( - future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex), + future: messageHandler(outerContext, profileOnion, contactHandle, byIndex: true, index: messageIndex), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data as Message; diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 8661305d..f7b112fc 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -220,8 +220,8 @@ class MessageRowState extends State with SingleTickerProviderStateMi ))))); var mark = Provider.of(context).newMarker; if (mark > 0 && - Provider.of(context).messageCache.length > mark && - Provider.of(context).messageCache[mark - 1]?.metadata.messageID == Provider.of(context).messageID) { + Provider.of(context).messageCache.indexedLength > mark && + Provider.of(context).messageCache.getByIndex(mark - 1)?.metadata.messageID == Provider.of(context).messageID) { return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); } else { return mr; From 589bc4c36cde956df74b333002f532a209ff1d11 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Thu, 20 Jan 2022 13:05:11 -0500 Subject: [PATCH 28/47] new lcg; cleanup --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- lib/models/contact.dart | 16 ++++++++-------- lib/models/message.dart | 18 +++++++++--------- lib/models/messagecache.dart | 5 ----- lib/views/messageview.dart | 2 +- 6 files changed, 20 insertions(+), 25 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index c6c568fa..c0ed5239 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-19-16-16-v1.5.4-11-g84d451f \ No newline at end of file +2022-01-20-12-53-v1.5.4-14-g6865ec1 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 8c390298..ee031103 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-19-21-15-v1.5.4-11-g84d451f \ No newline at end of file +2022-01-20-17-53-v1.5.4-14-g6865ec1 \ No newline at end of file diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 3793fb91..6e432f1a 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -21,7 +21,6 @@ class ContactInfoState extends ChangeNotifier { late Map> keys; int _newMarker = 0; DateTime _newMarkerClearAt = DateTime.now(); - //late List messageCache; late MessageCache messageCache; // todo: a nicer way to model contacts, groups and other "entities" @@ -56,7 +55,6 @@ class ContactInfoState extends ChangeNotifier { this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._server = server; this._archived = archived; - //this.messageCache = List.empty(growable: true); this.messageCache = new MessageCache(); keys = Map>(); } @@ -66,6 +64,7 @@ class ContactInfoState extends ChangeNotifier { String get savePeerHistory => this._savePeerHistory; String? get acnCircuit => this._acnCircuit; + set acnCircuit(String? acnCircuit) { this._acnCircuit = acnCircuit; notifyListeners(); @@ -92,6 +91,7 @@ class ContactInfoState extends ChangeNotifier { } bool get isGroup => this._isGroup; + set isGroup(bool newVal) { this._isGroup = newVal; notifyListeners(); @@ -112,12 +112,14 @@ class ContactInfoState extends ChangeNotifier { } String get status => this._status; + set status(String newVal) { this._status = newVal; notifyListeners(); } int get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { // don't reset newMarker position when unreadMessages is being cleared if (newVal > 0) { @@ -151,18 +153,21 @@ class ContactInfoState extends ChangeNotifier { } int get totalMessages => this._totalMessages; + set totalMessages(int newVal) { this._totalMessages = newVal; notifyListeners(); } String get imagePath => this._imagePath; + set imagePath(String newVal) { this._imagePath = newVal; notifyListeners(); } DateTime get lastMessageTime => this._lastMessageTime; + set lastMessageTime(DateTime newVal) { this._lastMessageTime = newVal; notifyListeners(); @@ -217,13 +222,8 @@ class ContactInfoState extends ChangeNotifier { } } - void bumpMessageCache() { - this.messageCache.bumpMessageCache(); - this.totalMessages += 1; - } - void ackCache(int messageID) { this.messageCache.ackCache(messageID); notifyListeners(); } -} \ No newline at end of file +} diff --git a/lib/models/message.dart b/lib/models/message.dart index 82fa7cd2..5ccfec23 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -141,15 +141,15 @@ Future fetchAndCacheMessageInfo(BuildContext context, String profi return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { try { dynamic messageWrapper = jsonDecode(rawMessageEnvelope); -// There are 2 conditions in which this error condition can be met: -// 1. The application == nil, in which case this instance of the UI is already -// broken beyond repair, and will either be replaced by a new version, or requires a complete -// restart. -// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. -// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the -// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one -// will find itself delayed. -// The second case is recoverable by tail-recursing this future. + // There are 2 conditions in which this error condition can be met: + // 1. The application == nil, in which case this instance of the UI is already + // broken beyond repair, and will either be replaced by a new version, or requires a complete + // restart. + // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. + // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the + // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one + // will find itself delayed. + // The second case is recoverable by tail-recursing this future. if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index 3c11e526..d8eaa999 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -51,11 +51,6 @@ class MessageCache { } } - // TODO inserting nulls travel down list causing fails for all - void bumpMessageCache() { - this.cacheByIndex.insert(0, null); - } - void ackCache(int messageID) { cache[messageID]?.metadata.ackd = true; } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index e2a22ba6..d1768161 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -227,9 +227,9 @@ class _MessageViewState extends State { Future.delayed(const Duration(milliseconds: 80), () { var profile = Provider.of(context, listen: false).profileOnion; var identifier = Provider.of(context, listen: false).identifier; - //Provider.of(context, listen: false).contactList.getContact(Provider.of(context, listen: false).identifier)?.bumpMessageCache(); fetchAndCacheMessageInfo(context, profile, identifier, byIndex: true, index: 0); Provider.of(context, listen: false).newMarker++; + Provider.of(context, listen: false).totalMessages += 1; // Resort the contact list... Provider.of(context, listen: false).contactList.updateLastMessageTime(Provider.of(context, listen: false).identifier, DateTime.now()); }); From 889d398343bd26e43f27d9ac55419fe1c2c34208 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Thu, 20 Jan 2022 13:37:09 -0500 Subject: [PATCH 29/47] add notifyListen to newMessage in contact; format --- lib/cwtch/cwtchNotifier.dart | 13 ++++++- lib/models/appstate.dart | 2 +- lib/models/contact.dart | 1 + lib/models/contactlist.dart | 2 +- lib/models/message.dart | 51 ++++++-------------------- lib/models/messagecache.dart | 3 +- lib/models/messages/quotedmessage.dart | 3 +- lib/views/messageview.dart | 4 +- 8 files changed, 30 insertions(+), 49 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 74f395ab..ea93c064 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -146,7 +146,17 @@ class CwtchNotifier { String? contenthash = data['ContentHash']; var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; - profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data["Data"], contenthash, selectedConversation, ); + profileCN.getProfile(data["ProfileOnion"])?.contactList.newMessage( + identifier, + messageID, + timestamp, + senderHandle, + senderImage, + isAuto, + data["Data"], + contenthash, + selectedConversation, + ); break; case "PeerAcknowledgement": @@ -188,7 +198,6 @@ class CwtchNotifier { String? contenthash = data['ContentHash']; var selectedConversation = appState.selectedProfile == data["ProfileOnion"] && appState.selectedConversation == identifier; - // Only bother to do anything if we know about the group and the provided index is greater than our current total... if (currentTotal != null && idx >= currentTotal) { // TODO: There are 2 timestamps associated with a new group message - time sent and time received. diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 39ae8454..8386b8dd 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -68,4 +68,4 @@ class AppState extends ChangeNotifier { } bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; -} \ No newline at end of file +} diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 6e432f1a..d39aa193 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -220,6 +220,7 @@ class ContactInfoState extends ChangeNotifier { if (isOnline() == false) { status = "Authenticated"; } + notifyListeners(); } void ackCache(int messageID) { diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart index d54dfdf3..f4aaadcf 100644 --- a/lib/models/contactlist.dart +++ b/lib/models/contactlist.dart @@ -127,4 +127,4 @@ class ContactListState extends ChangeNotifier { getContact(identifier)?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation); updateLastMessageTime(identifier, DateTime.now()); } -} \ No newline at end of file +} diff --git a/lib/models/message.dart b/lib/models/message.dart index 5ccfec23..52070666 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -74,14 +74,14 @@ Future messageHandler(BuildContext context, String profileOnion, int co } // Hit cache - MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); + MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); if (messageInfo != null) { return Future.value(compileOverlay(messageInfo.metadata, messageInfo.wrapper)); } // Fetch and Cache - var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); - return messageInfoFuture.then( (MessageInfo? messageInfo) { + var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); + return messageInfoFuture.then((MessageInfo? messageInfo) { if (messageInfo != null) { return compileOverlay(messageInfo.metadata, messageInfo.wrapper); } else { @@ -91,7 +91,7 @@ Future messageHandler(BuildContext context, String profileOnion, int co } MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, -{bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { + {bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { // Hit cache try { var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; @@ -116,26 +116,17 @@ MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, } Future fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, -{bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { + {bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { // Load and cache try { Future rawMessageEnvelopeFuture; if (byID) { - rawMessageEnvelopeFuture = Provider - .of(context, listen: false) - .cwtch - .GetMessageByID(profileOnion, conversationIdentifier, id!); + rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, id!); } else if (byHash) { - rawMessageEnvelopeFuture = Provider - .of(context, listen: false) - .cwtch - .GetMessageByContentHash(profileOnion, conversationIdentifier, hash!); + rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash!); } else { - rawMessageEnvelopeFuture = Provider - .of(context, listen: false) - .cwtch - .GetMessage(profileOnion, conversationIdentifier, index!); + rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index!); } return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { @@ -153,12 +144,7 @@ Future fetchAndCacheMessageInfo(BuildContext context, String profi if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, - index: index, - byID: byID, - id: id, - byHash: byHash, - hash: hash).then((value) => value); + return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash).then((value) => value); }); } @@ -173,25 +159,10 @@ Future fetchAndCacheMessageInfo(BuildContext context, String profi var signature = messageWrapper['Signature']; var contenthash = messageWrapper['ContentHash']; var localIndex = messageWrapper['LocalIndex']; - var metadata = MessageMetadata( - profileOnion, - conversationIdentifier, - messageID, - timestamp, - senderHandle, - senderImage, - signature, - attributes, - ackd, - error, - false); + var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false); var messageInfo = new MessageInfo(metadata, messageWrapper['Message']); - var cache = Provider - .of(context, listen: false) - .contactList - .getContact(conversationIdentifier) - ?.messageCache; + var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; if (cache != null) { if (byID) { diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index d8eaa999..a2deae4e 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -26,6 +26,7 @@ class MessageCache { } return cache[cacheByIndex[index]]; } + MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]]; void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash) { @@ -54,4 +55,4 @@ class MessageCache { void ackCache(int messageID) { cache[messageID]?.metadata.ackd = true; } -} \ No newline at end of file +} diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 68615f18..7f5053d9 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -59,8 +59,7 @@ class QuotedMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - return MessageRow( - QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, byHash: true, hash: message["quotedHash"])), + return MessageRow(QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, byHash: true, hash: message["quotedHash"])), key: key); }); } catch (e) { diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index d1768161..bc811f67 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -284,8 +284,8 @@ class _MessageViewState extends State { var children; if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { var quoted = FutureBuilder( - future: - messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, id: Provider.of(context).selectedIndex!, byID: true), + future: messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, + id: Provider.of(context).selectedIndex!, byID: true), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data! as Message; From ccdd7d0e277d0d6563f166470c7810974015b5b6 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Thu, 20 Jan 2022 15:58:14 -0500 Subject: [PATCH 30/47] remove byType bools and replace with interface and structs for type safety --- lib/models/message.dart | 111 ++++++++++++++++--------- lib/models/messages/quotedmessage.dart | 3 +- lib/views/messageview.dart | 5 +- lib/widgets/messagelist.dart | 2 +- 4 files changed, 74 insertions(+), 47 deletions(-) diff --git a/lib/models/message.dart b/lib/models/message.dart index 52070666..6ada0469 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:cwtch/config.dart'; +import 'package:cwtch/cwtch/cwtch.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; @@ -61,26 +62,76 @@ Message compileOverlay(MessageMetadata metadata, String messageData) { } } -Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, - {bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { - var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", {}, false, true, false); - if (!byIndex && !byID && !byHash) { - EnvironmentConfig.debugLog("Error calling messageHandler: one of byIndex, byID, byHash must be set"); - return Future.value(MalformedMessage(malformedMetadata)); - } - if ((byID && id == null) || (byIndex && index == null) || (byHash && hash == null)) { - EnvironmentConfig.debugLog("Error calling messageHandler: byType needs corresponding value and it was not set"); - return Future.value(MalformedMessage(malformedMetadata)); +abstract class CacheHandler { + MessageInfo? lookup(MessageCache cache); + Future fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier); + void add(MessageCache cache, MessageInfo messageInfo, String contenthash); +} + +class ByIndex implements CacheHandler { + int index; + + ByIndex(this.index); + + MessageInfo? lookup(MessageCache cache) { + return cache.getByIndex(index); } + Future fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) { + return cwtch.GetMessage(profileOnion, conversationIdentifier, index); + } + + void add(MessageCache cache, MessageInfo messageInfo, String contenthash) { + cache.add(messageInfo, index, contenthash); + } +} + +class ById implements CacheHandler { + int id; + + ById(this.id); + + MessageInfo? lookup(MessageCache cache) { + return cache.getById(id); + } + + Future fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) { + return cwtch.GetMessageByID(profileOnion, conversationIdentifier, id); + } + + void add(MessageCache cache, MessageInfo messageInfo, String contenthash) { + cache.addUnindexed(messageInfo, contenthash); + } +} + +class ByContentHash implements CacheHandler { + String hash; + + ByContentHash(this.hash); + + MessageInfo? lookup(MessageCache cache) { + return cache.getByContentHash(hash); + } + + Future fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) { + return cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash); + } + + void add(MessageCache cache, MessageInfo messageInfo, String contenthash) { + cache.addUnindexed(messageInfo, contenthash); + } +} + +Future messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) { + var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", {}, false, true, false); // Hit cache - MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); + MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, cacheHandler); if (messageInfo != null) { return Future.value(compileOverlay(messageInfo.metadata, messageInfo.wrapper)); } // Fetch and Cache - var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash); + var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler); return messageInfoFuture.then((MessageInfo? messageInfo) { if (messageInfo != null) { return compileOverlay(messageInfo.metadata, messageInfo.wrapper); @@ -90,20 +141,12 @@ Future messageHandler(BuildContext context, String profileOnion, int co }); } -MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, - {bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { +MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) { // Hit cache try { var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; if (cache != null) { - MessageInfo? messageInfo = null; - if (byID) { - messageInfo = cache.getById(id!); - } else if (byHash) { - messageInfo = cache.getByContentHash(hash!); - } else { - messageInfo = cache.getByIndex(index!); - } + MessageInfo? messageInfo = cacheHandler.lookup(cache); if (messageInfo != null) { return messageInfo; } @@ -115,19 +158,12 @@ MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, return null; } -Future fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, - {bool byIndex = false, int? index, bool byID = false, int? id, bool byHash = false, String? hash}) { +Future fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) { // Load and cache try { Future rawMessageEnvelopeFuture; - if (byID) { - rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessageByID(profileOnion, conversationIdentifier, id!); - } else if (byHash) { - rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash!); - } else { - rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, conversationIdentifier, index!); - } + rawMessageEnvelopeFuture = cacheHandler.fetch(Provider.of(context, listen: false).cwtch, profileOnion, conversationIdentifier); return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { try { @@ -144,11 +180,11 @@ Future fetchAndCacheMessageInfo(BuildContext context, String profi if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { return Future.delayed(Duration(seconds: 2), () { print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, byIndex: byIndex, index: index, byID: byID, id: id, byHash: byHash, hash: hash).then((value) => value); + return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler); }); } -// Construct the initial metadata + // Construct the initial metadata var messageID = messageWrapper['ID']; var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; var senderHandle = messageWrapper['PeerID']; @@ -163,15 +199,8 @@ Future fetchAndCacheMessageInfo(BuildContext context, String profi var messageInfo = new MessageInfo(metadata, messageWrapper['Message']); var cache = Provider.of(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; - if (cache != null) { - if (byID) { - cache.addUnindexed(messageInfo, contenthash); - } else if (byHash) { - cache.addUnindexed(messageInfo, contenthash); - } else { - cache.add(messageInfo, index!, contenthash); - } + cacheHandler.add(cache, messageInfo, contenthash); } return messageInfo; diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 7f5053d9..c43ac12c 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -59,8 +59,7 @@ class QuotedMessage extends Message { return ChangeNotifierProvider.value( value: this.metadata, builder: (bcontext, child) { - return MessageRow(QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, byHash: true, hash: message["quotedHash"])), - key: key); + return MessageRow(QuotedMessageBubble(message["body"], messageHandler(bcontext, metadata.profileOnion, metadata.conversationIdentifier, ByContentHash(message["quotedHash"]))), key: key); }); } catch (e) { return MalformedBubble(); diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index bc811f67..dd81c1c2 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -227,7 +227,7 @@ class _MessageViewState extends State { Future.delayed(const Duration(milliseconds: 80), () { var profile = Provider.of(context, listen: false).profileOnion; var identifier = Provider.of(context, listen: false).identifier; - fetchAndCacheMessageInfo(context, profile, identifier, byIndex: true, index: 0); + fetchAndCacheMessageInfo(context, profile, identifier, ByIndex(0)); Provider.of(context, listen: false).newMarker++; Provider.of(context, listen: false).totalMessages += 1; // Resort the contact list... @@ -284,8 +284,7 @@ class _MessageViewState extends State { var children; if (Provider.of(context).selectedConversation != null && Provider.of(context).selectedIndex != null) { var quoted = FutureBuilder( - future: messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, - id: Provider.of(context).selectedIndex!, byID: true), + future: messageHandler(context, Provider.of(context).selectedProfile!, Provider.of(context).selectedConversation!, ById(Provider.of(context).selectedIndex!)), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data! as Message; diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 4756381d..da8b3ea5 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -83,7 +83,7 @@ class _MessageListState extends State { var messageIndex = index; return FutureBuilder( - future: messageHandler(outerContext, profileOnion, contactHandle, byIndex: true, index: messageIndex), + future: messageHandler(outerContext, profileOnion, contactHandle, ByIndex(messageIndex)), builder: (context, snapshot) { if (snapshot.hasData) { var message = snapshot.data as Message; From 797279d6d75ce493aea1e6a1b28ad668cdf47719 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 20 Jan 2022 13:59:54 -0800 Subject: [PATCH 31/47] Enable Sender Side Image Previews --- lib/cwtch/cwtchNotifier.dart | 11 +++++++++-- lib/models/profile.dart | 11 +++++++++++ lib/widgets/filebubble.dart | 30 ++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index ea93c064..ddad1088 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -98,8 +98,10 @@ class CwtchNotifier { } if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(data["ConversationID"])) == null) { profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"], - blocked: false, // we created - accepted: true, // we created + blocked: false, + // we created + accepted: true, + // we created imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, @@ -244,6 +246,11 @@ class CwtchNotifier { case "UpdatedProfileAttribute": if (data["Key"] == "public.profile.name") { profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"]; + } else if (data["Key"].toString().endsWith(".path")) { + // local.conversation.filekey.path + List keyparts = data["Key"].toString().split("."); + String filekey = keyparts[2] + "." + keyparts[3]; + profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathDangerous(filekey, data["Data"]); } else { EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}"); } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index ddca5aba..9ae039ad 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -253,7 +253,18 @@ class ProfileInfoState extends ChangeNotifier { } } + void downloadSetPathDangerous(String fileKey, String path) { + this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.chunksDownloaded = 1; + this._downloads[fileKey]!.gotManifest = true; + this._downloads[fileKey]!.complete = true; + this._downloads[fileKey]!.downloadedTo = path; + notifyListeners(); + } + String? downloadFinalPath(String fileKey) { + var path = this._downloads[fileKey]; return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 1d6c347c..dc0b4cfa 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -52,16 +52,28 @@ class FileBubbleState extends State { var borderRadiousEh = 15.0; var showFileSharing = Provider.of(context, listen: false).isExperimentEnabled(FileSharingExperiment); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); - var downloadComplete = Provider.of(context).downloadComplete(widget.fileKey()); + var metadata = Provider.of(context); + var downloadComplete = metadata.attributes["filepath"] != null || Provider.of(context).downloadComplete(widget.fileKey()); var downloadInterrupted = Provider.of(context).downloadInterrupted(widget.fileKey()); var path = Provider.of(context).downloadFinalPath(widget.fileKey()); - if (downloadComplete) { - var lpath = path!.toLowerCase(); + + // If we haven't stored the filepath in message attributes then save it + if (downloadComplete && path == null && metadata.attributes["filepath"] != null) { + path = metadata.attributes["filepath"]; + } else if (downloadComplete && path != null && metadata.attributes["filepath"] == null) { + if (metadata.attributes["filepath"] == null) { + Provider.of(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", path); + } + } + + var fileKey = widget.fileKey(); + if (downloadComplete && path != null) { + var lpath = path.toLowerCase(); if (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp")) { if (myFile == null) { setState(() { - myFile = new File(path); + myFile = new File(path!); }); } } @@ -81,6 +93,8 @@ class FileBubbleState extends State { } else { senderDisplayStr = Provider.of(context).senderHandle; } + } else { + senderIsContact = true; } return LayoutBuilder(builder: (bcontext, constraints) { var wdgSender = Visibility( @@ -98,13 +112,9 @@ class FileBubbleState extends State { if (!showFileSharing) { wdgDecorations = Text('\u202F'); - } else if (fromMe) { - wdgDecorations = Visibility( - visible: widget.interactive, - child: MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate)); - } else if (downloadComplete) { + } else if (downloadComplete && path != null) { // in this case, whatever marked download.complete would have also set the path - if (Provider.of(context).shouldPreview(path!)) { + if (Provider.of(context).shouldPreview(path)) { isPreview = true; wdgDecorations = Center( child: MouseRegion( From d095971cb3c2965b3c3cdba1e9102ef56bd97d82 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 20 Jan 2022 14:19:06 -0800 Subject: [PATCH 32/47] Sender side previews - fixing up PR comments --- lib/cwtch/cwtchNotifier.dart | 14 +++++++------- lib/models/profile.dart | 4 ++-- lib/widgets/filebubble.dart | 7 +------ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index ddad1088..f8ac337c 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -98,10 +98,8 @@ class CwtchNotifier { } if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(int.parse(data["ConversationID"])) == null) { profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"], - blocked: false, - // we created - accepted: true, - // we created + blocked: false, // we created + accepted: true, // we created imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, @@ -246,11 +244,13 @@ class CwtchNotifier { case "UpdatedProfileAttribute": if (data["Key"] == "public.profile.name") { profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"]; - } else if (data["Key"].toString().endsWith(".path")) { + } else if (data["Key"].toString().startsWith("local.filesharing.") && data["Key"].toString().endsWith(".path")) { // local.conversation.filekey.path List keyparts = data["Key"].toString().split("."); - String filekey = keyparts[2] + "." + keyparts[3]; - profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathDangerous(filekey, data["Data"]); + if (keyparts.length == 5) { + String filekey = keyparts[2] + "." + keyparts[3]; + profileCN.getProfile(data["ProfileOnion"])?.downloadSetPathForSender(filekey, data["Data"]); + } } else { EnvironmentConfig.debugLog("unhandled set attribute event: ${data['Key']}"); } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 9ae039ad..f11c9b0b 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -253,7 +253,8 @@ class ProfileInfoState extends ChangeNotifier { } } - void downloadSetPathDangerous(String fileKey, String path) { + // set the download path for the sender + void downloadSetPathForSender(String fileKey, String path) { this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); this._downloads[fileKey]!.timeEnd = DateTime.now(); this._downloads[fileKey]!.chunksDownloaded = 1; @@ -264,7 +265,6 @@ class ProfileInfoState extends ChangeNotifier { } String? downloadFinalPath(String fileKey) { - var path = this._downloads[fileKey]; return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index dc0b4cfa..a79e884c 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -62,12 +62,9 @@ class FileBubbleState extends State { if (downloadComplete && path == null && metadata.attributes["filepath"] != null) { path = metadata.attributes["filepath"]; } else if (downloadComplete && path != null && metadata.attributes["filepath"] == null) { - if (metadata.attributes["filepath"] == null) { - Provider.of(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", path); - } + Provider.of(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", path); } - var fileKey = widget.fileKey(); if (downloadComplete && path != null) { var lpath = path.toLowerCase(); if (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp")) { @@ -93,8 +90,6 @@ class FileBubbleState extends State { } else { senderDisplayStr = Provider.of(context).senderHandle; } - } else { - senderIsContact = true; } return LayoutBuilder(builder: (bcontext, constraints) { var wdgSender = Visibility( From 6364ebffc6b53f93878b564b92c26be451c2c5be Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 20 Jan 2022 14:32:35 -0800 Subject: [PATCH 33/47] Update lcg --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index c0ed5239..124e8a8e 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-20-12-53-v1.5.4-14-g6865ec1 \ No newline at end of file +2022-01-20-17-22-v1.5.4-16-ge0e1a4b \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index ee031103..3513fd2f 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-20-17-53-v1.5.4-14-g6865ec1 \ No newline at end of file +2022-01-20-22-22-v1.5.4-16-ge0e1a4b \ No newline at end of file From 13c1a52442cd4b3a3ac4800ed8e3f5386cbe1f08 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 20 Jan 2022 14:42:45 -0800 Subject: [PATCH 34/47] Only allow path override for senders --- lib/models/profile.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/models/profile.dart b/lib/models/profile.dart index f11c9b0b..dc1b6c58 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -255,13 +255,16 @@ class ProfileInfoState extends ChangeNotifier { // set the download path for the sender void downloadSetPathForSender(String fileKey, String path) { - this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.chunksDownloaded = 1; - this._downloads[fileKey]!.gotManifest = true; - this._downloads[fileKey]!.complete = true; - this._downloads[fileKey]!.downloadedTo = path; - notifyListeners(); + // only allow this override if we are the sender... + if (this._downloads.containsKey(fileKey) == false) { + this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.chunksDownloaded = 1; + this._downloads[fileKey]!.gotManifest = true; + this._downloads[fileKey]!.complete = true; + this._downloads[fileKey]!.downloadedTo = path; + notifyListeners(); + } } String? downloadFinalPath(String fileKey) { From 99315219101c84ae9423f10e5980ec175572dc80 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 20 Jan 2022 14:52:31 -0800 Subject: [PATCH 35/47] Clean up sender side image preview --- lib/widgets/filebubble.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index a79e884c..79b73afd 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -194,7 +194,7 @@ class FileBubbleState extends State { crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: fromMe ? [wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)] : [wdgSender, isPreview ? Container() : wdgMessage, wdgDecorations]), + children: [wdgSender, isPreview ? Container() : wdgMessage, wdgDecorations]), )); }); } From 92374ad112dff0b332662103d454cd8259060f87 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 21 Jan 2022 10:12:49 -0800 Subject: [PATCH 36/47] Only override path for Sender, not any other attributes. For auto-downloads both the sender and receiver set the path before the UI can set download state. As such we need to be careful about how we let the sender know about the filekey/path. --- lib/models/profile.dart | 12 +++++------- lib/widgets/filebubble.dart | 13 ++++++------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/models/profile.dart b/lib/models/profile.dart index dc1b6c58..98783f12 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -255,16 +255,14 @@ class ProfileInfoState extends ChangeNotifier { // set the download path for the sender void downloadSetPathForSender(String fileKey, String path) { - // only allow this override if we are the sender... + // we may trigger this event for auto-downloaded receivers too, + // as such we don't assume anything else about the file...other than that + // it exists. if (this._downloads.containsKey(fileKey) == false) { + // this will be overwritten by download update if the file is being downloaded this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.chunksDownloaded = 1; - this._downloads[fileKey]!.gotManifest = true; - this._downloads[fileKey]!.complete = true; - this._downloads[fileKey]!.downloadedTo = path; - notifyListeners(); } + this._downloads[fileKey]!.downloadedTo = path; } String? downloadFinalPath(String fileKey) { diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 79b73afd..4d91de6d 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -53,18 +53,19 @@ class FileBubbleState extends State { var showFileSharing = Provider.of(context, listen: false).isExperimentEnabled(FileSharingExperiment); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); var metadata = Provider.of(context); - var downloadComplete = metadata.attributes["filepath"] != null || Provider.of(context).downloadComplete(widget.fileKey()); - var downloadInterrupted = Provider.of(context).downloadInterrupted(widget.fileKey()); - var path = Provider.of(context).downloadFinalPath(widget.fileKey()); // If we haven't stored the filepath in message attributes then save it - if (downloadComplete && path == null && metadata.attributes["filepath"] != null) { + if (metadata.attributes["filepath"] != null) { path = metadata.attributes["filepath"]; - } else if (downloadComplete && path != null && metadata.attributes["filepath"] == null) { + } else if (path != null && metadata.attributes["filepath"] == null) { Provider.of(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", path); } + // the file is downloaded when it is from the sender AND the path is known OR when we get an explicit downloadComplete + var downloadComplete = (fromMe && path != null) || Provider.of(context).downloadComplete(widget.fileKey()); + var downloadInterrupted = Provider.of(context).downloadInterrupted(widget.fileKey()); + if (downloadComplete && path != null) { var lpath = path.toLowerCase(); if (lpath.endsWith(".jpg") || lpath.endsWith(".jpeg") || lpath.endsWith(".png") || lpath.endsWith(".gif") || lpath.endsWith(".webp") || lpath.endsWith(".bmp")) { @@ -209,7 +210,6 @@ class FileBubbleState extends State { if (Platform.isAndroid) { Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); Provider.of(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true"); - //Provider.of(context, listen: false).attributes |= 0x02; ContactInfoState? contact = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey()); @@ -225,7 +225,6 @@ class FileBubbleState extends State { var manifestPath = file.path + ".manifest"; Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); Provider.of(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true"); - //Provider.of(context, listen: false).flags |= 0x02; ContactInfoState? contact = Provider.of(context, listen: false).contactList.findContact(Provider.of(context).senderHandle); if (contact != null) { Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey()); From e359afbdabcdc6de8579aac6d5b1ffba7e829a2f Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 21 Jan 2022 12:08:23 -0800 Subject: [PATCH 37/47] notify listeners --- lib/models/profile.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 98783f12..e79eb573 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -174,6 +174,7 @@ class ProfileInfoState extends ChangeNotifier { void downloadInit(String fileKey, int numChunks) { this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); + notifyListeners(); } void downloadUpdate(String fileKey, int progress, int numChunks) { @@ -239,6 +240,7 @@ class ProfileInfoState extends ChangeNotifier { void downloadMarkResumed(String fileKey) { if (this._downloads.containsKey(fileKey)) { this._downloads[fileKey]!.interrupted = false; + notifyListeners(); } } @@ -258,7 +260,7 @@ class ProfileInfoState extends ChangeNotifier { // we may trigger this event for auto-downloaded receivers too, // as such we don't assume anything else about the file...other than that // it exists. - if (this._downloads.containsKey(fileKey) == false) { + if (!this._downloads.containsKey(fileKey)) { // this will be overwritten by download update if the file is being downloaded this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); } From d27cc0e64e754bf61a7293b3430b334b15d8d906 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 21 Jan 2022 12:09:58 -0800 Subject: [PATCH 38/47] More notify listeners --- lib/models/profile.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/models/profile.dart b/lib/models/profile.dart index e79eb573..d79617ef 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -252,6 +252,7 @@ class ProfileInfoState extends ChangeNotifier { void downloadSetPath(String fileKey, String path) { if (this._downloads.containsKey(fileKey)) { this._downloads[fileKey]!.downloadedTo = path; + notifyListeners(); } } @@ -265,6 +266,7 @@ class ProfileInfoState extends ChangeNotifier { this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); } this._downloads[fileKey]!.downloadedTo = path; + notifyListeners(); } String? downloadFinalPath(String fileKey) { From 748326e13f8420ff657af7e864ad8f98242a6043 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 21 Jan 2022 13:17:16 -0800 Subject: [PATCH 39/47] Fix #330 - Multiple file browser windows are opened. --- lib/models/appstate.dart | 7 +++++++ lib/views/messageview.dart | 21 +++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 8386b8dd..34d4c383 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -13,6 +13,7 @@ class AppState extends ChangeNotifier { int _hoveredIndex = -1; int? _selectedIndex; bool _unreadMessagesBelow = false; + bool _disableFilePicker = false; void SetCwtchInit() { cwtchInit = true; @@ -47,6 +48,12 @@ class AppState extends ChangeNotifier { notifyListeners(); } + bool get disableFilePicker => _disableFilePicker; + set disableFilePicker(bool newVal) { + this._disableFilePicker = newVal; + notifyListeners(); + } + // Never use this for message lookup - can be a non-indexed value // e.g. -1 int get hoveredIndex => _hoveredIndex; diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index dd81c1c2..9bdcacae 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; -import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/chatmessage.dart'; @@ -23,7 +22,6 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:path/path.dart' show basename; import '../main.dart'; import '../settings.dart'; @@ -89,11 +87,13 @@ class _MessageViewState extends State { if (showFileSharing) { appBarButtons.add(IconButton( splashRadius: Material.defaultSplashRadius / 2, - icon: Icon(Icons.attach_file, size: 24), + icon: Icon(Icons.attach_file, size: 24, color: Provider.of(context).theme.mainTextColor), tooltip: AppLocalizations.of(context)!.tooltipSendFile, - onPressed: () { - _showFilePicker(context); - }, + onPressed: Provider.of(context).disableFilePicker + ? null + : () { + _showFilePicker(context); + }, )); } appBarButtons.add(IconButton( @@ -392,7 +392,16 @@ class _MessageViewState extends State { void _showFilePicker(BuildContext ctx) async { imagePreview = null; + + // only allow one file picker at a time + // note: ideally we would destroy file picker when leaving a conversation + // but we don't currently have that option. + // we need to store AppState in a variable because ctx might be destroyed + // while awaiting for pickFiles. + var appstate = Provider.of(ctx, listen: false); + appstate.disableFilePicker = true; FilePickerResult? result = await FilePicker.platform.pickFiles(); + appstate.disableFilePicker = false; if (result != null) { File file = File(result.files.first.path); // We have a maximum number of bytes we can represent in terms of From def222a8abe9558fa2f013208fb22690363f4fbb Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 21 Jan 2022 13:40:23 -0800 Subject: [PATCH 40/47] upgrade flutter file picker --- lib/views/messageview.dart | 7 ++++--- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 9bdcacae..ade71beb 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -400,10 +400,11 @@ class _MessageViewState extends State { // while awaiting for pickFiles. var appstate = Provider.of(ctx, listen: false); appstate.disableFilePicker = true; - FilePickerResult? result = await FilePicker.platform.pickFiles(); + // currently lockParentWindow only works on Windows... + FilePickerResult? result = await FilePicker.platform.pickFiles(lockParentWindow: true); appstate.disableFilePicker = false; - if (result != null) { - File file = File(result.files.first.path); + if (result != null && result.files.first.path != null) { + File file = File(result.files.first.path!); // We have a maximum number of bytes we can represent in terms of // a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25) if (file.lengthSync() <= 10737418240) { diff --git a/pubspec.lock b/pubspec.lock index a967a6be..22a566d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -112,14 +112,14 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.3.2" file_picker_desktop: dependency: "direct main" description: name: file_picker_desktop url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 03d6d590..47e0b916 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: flutter_test: sdk: flutter scrollable_positioned_list: ^0.2.0-nullsafety.0 - file_picker: ^4.0.1 + file_picker: ^4.3.2 file_picker_desktop: ^1.1.0 url_launcher: ^6.0.12 From a9d272e4143f7e8ed1f2d06d85f9d23a13a39f21 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Fri, 21 Jan 2022 18:48:34 -0500 Subject: [PATCH 41/47] bump android or to 0.4.6.9 --- fetch-tor.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fetch-tor.sh b/fetch-tor.sh index 90e26e33..925dd60c 100755 --- a/fetch-tor.sh +++ b/fetch-tor.sh @@ -4,9 +4,9 @@ wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor chmod a+x linux/tor mkdir -p android/app/src/main/jniLibs/arm64-v8a -wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm64_pie -O android/app/src/main/jniLibs/arm64-v8a/libtor.so +wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.6.9-arm64 -O android/app/src/main/jniLibs/arm64-v8a/libtor.so chmod a+x android/app/src/main/jniLibs/arm64-v8a/libtor.so mkdir -p android/app/src/main/jniLibs/armeabi-v7a -wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm_pie -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so +wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.6.9-arm7 -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so chmod a+x android/app/src/main/jniLibs/armeabi-v7a/libtor.so From c838176e3b7151425fcd716ce27d8fb05109489c Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Mon, 24 Jan 2022 16:03:46 -0800 Subject: [PATCH 42/47] add desktoasts windows notifications --- lib/main.dart | 2 +- lib/notification_manager.dart | 53 ++++++++++++++++--- pubspec.lock | 7 +++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 6 files changed, 58 insertions(+), 9 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 664464e7..aea790ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -70,7 +70,7 @@ class FlwtchState extends State { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList); cwtch = CwtchFfi(cwtchNotifier); } else { - var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList); + var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList); cwtch = CwtchFfi(cwtchNotifier); } print("initState: invoking cwtch.Start()"); diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart index fa5de8c3..f01659be 100644 --- a/lib/notification_manager.dart +++ b/lib/notification_manager.dart @@ -1,6 +1,11 @@ +import 'dart:io'; + +import 'package:desktoasts/desktoasts.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:path/path.dart' as path; +import 'config.dart'; + // NotificationsManager provides a wrapper around platform specific notifications logic. abstract class NotificationsManager { Future notify(String message); @@ -26,15 +31,47 @@ class LinuxNotificationsManager implements NotificationsManager { } } +// Windows Notification Manager uses https://pub.dev/packages/desktoasts to implement +// windows notifications +class WindowsNotificationManager implements NotificationsManager { + late ToastService service; + + WindowsNotificationManager() { + service = new ToastService( + appName: 'Cwtch', + companyName: 'Open Privacy Research Society', + productName: 'Cwtch', + ); + } + + Future notify(String message) async { + Toast toast = new Toast( + type: ToastType.text01, + title: 'Cwtch', + subtitle: message, + ); + service.show(toast); + } +} + NotificationsManager newDesktopNotificationsManager() { - try { - // Test that we can actually access DBUS. Otherwise return a null - // notifications manager... - NotificationsClient client = NotificationsClient(); - client.getCapabilities(); - return LinuxNotificationsManager(client); - } catch (e) { - print("Attempted to access DBUS for notifications but failed. Switching off notifications."); + if (Platform.isLinux) { + try { + // Test that we can actually access DBUS. Otherwise return a null + // notifications manager... + NotificationsClient client = NotificationsClient(); + client.getCapabilities(); + return LinuxNotificationsManager(client); + } catch (e) { + EnvironmentConfig.debugLog( + "Attempted to access DBUS for notifications but failed. Switching off notifications."); + } + } else if (Platform.isWindows) { + try { + return WindowsNotificationManager(); + } catch (e) { + EnvironmentConfig.debugLog("Failed to create Windows desktoasts notification manager"); + } } return NullNotificationsManager(); } diff --git a/pubspec.lock b/pubspec.lock index 22a566d6..8f1137b3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.5.2" + desktoasts: + dependency: "direct main" + description: + name: desktoasts + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" desktop_notifications: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 47e0b916..532fffce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: file_picker: ^4.3.2 file_picker_desktop: ^1.1.0 url_launcher: ^6.0.12 + desktoasts: ^0.0.2 dev_dependencies: msix: ^2.1.3 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4f788487..6859cc41 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktoastsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktoastsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 411af46d..cb499b22 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktoasts url_launcher_windows ) From 23b6eddf6a25fef56011346fc37d5fc156d8b6f0 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 25 Jan 2022 10:52:18 -0500 Subject: [PATCH 43/47] bump windows tor --- .drone.yml | 5 ++--- fetch-tor-win.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.drone.yml b/.drone.yml index 3d3b96a7..71e49b10 100644 --- a/.drone.yml +++ b/.drone.yml @@ -197,10 +197,9 @@ steps: - name: fetch image: openpriv/flutter-desktop:windows-sdk30-fstable-2.8.1 commands: - - powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip" - - powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }" - git describe --tags --abbrev=1 > VERSION - powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE + - .\fetch-tor-win.ps1 - .\fetch-libcwtch-go.ps1 - name: build-windows @@ -361,4 +360,4 @@ trigger: branch: trunk event: - push - - pull_request \ No newline at end of file + - pull_request diff --git a/fetch-tor-win.ps1 b/fetch-tor-win.ps1 index ba275cbc..1827f3b5 100644 --- a/fetch-tor-win.ps1 +++ b/fetch-tor-win.ps1 @@ -1,6 +1,6 @@ -Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip +Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.9.zip -OutFile tor.zip -if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' } +if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne 'bd99de56ef5ef9516410783ce48d52311093b26e718bf3d0a94efbd754d1cf2d12543f096139d9c289985349d26ee89b2308be5927fa1b410ff4f7f3129d6830' ) { Write-Error 'tor.zip sha512sum mismatch' } Expand-Archive -Path tor.zip -DestinationPath Tor From c3bc961a47b1f88e54878cc9c53945218acbcda8 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Wed, 26 Jan 2022 08:31:07 -0800 Subject: [PATCH 44/47] add window_manager plug in to get desktop active state to gate windows notifications; also add spam prevention to windows notifications --- lib/main.dart | 17 +++++++++++++++- lib/models/appstate.dart | 7 +++++++ lib/notification_manager.dart | 38 ++++++++++++++++++++++++++++------- pubspec.lock | 7 +++++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index aea790ad..22946d4f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:cwtch/settings.dart'; import 'package:cwtch/torstatus.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; import 'licenses.dart'; @@ -45,7 +46,7 @@ class Flwtch extends StatefulWidget { FlwtchState createState() => FlwtchState(); } -class FlwtchState extends State { +class FlwtchState extends State with WindowListener { final TextStyle biggerFont = const TextStyle(fontSize: 18); late Cwtch cwtch; late ProfileListState profs; @@ -56,6 +57,7 @@ class FlwtchState extends State { @override initState() { print("initState: running..."); + windowManager.addListener(this); super.initState(); print("initState: registering notification, shutdown handlers..."); @@ -203,8 +205,21 @@ class FlwtchState extends State { } } + // using windowManager flutter plugin until proper lifecycle management lands in desktop + + @override + void onWindowFocus() { + globalAppState.focus = true; + } + + @override + void onWindowBlur() { + globalAppState.focus = false; + } + @override void dispose() { + windowManager.removeListener(this); cwtch.dispose(); super.dispose(); } diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 34d4c383..5a2293c3 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -14,6 +14,7 @@ class AppState extends ChangeNotifier { int? _selectedIndex; bool _unreadMessagesBelow = false; bool _disableFilePicker = false; + bool _focus = true; void SetCwtchInit() { cwtchInit = true; @@ -74,5 +75,11 @@ class AppState extends ChangeNotifier { notifyListeners(); } + bool get focus => _focus; + set focus(bool newVal) { + _focus = newVal; + notifyListeners(); + } + bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; } diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart index f01659be..aa7b84a1 100644 --- a/lib/notification_manager.dart +++ b/lib/notification_manager.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:cwtch/main.dart'; import 'package:desktoasts/desktoasts.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:path/path.dart' as path; @@ -22,9 +23,11 @@ class NullNotificationsManager implements NotificationsManager { class LinuxNotificationsManager implements NotificationsManager { int previous_id = 0; late NotificationsClient client; + LinuxNotificationsManager(NotificationsClient client) { this.client = client; } + Future notify(String message) async { var iconPath = Uri.file(path.join(path.current, "cwtch.png")); client.notify(message, appName: "cwtch", appIcon: iconPath.toString(), replacesId: this.previous_id).then((Notification value) => previous_id = value.id); @@ -35,22 +38,43 @@ class LinuxNotificationsManager implements NotificationsManager { // windows notifications class WindowsNotificationManager implements NotificationsManager { late ToastService service; + bool active = false; WindowsNotificationManager() { service = new ToastService( - appName: 'Cwtch', + appName: 'cwtch', companyName: 'Open Privacy Research Society', productName: 'Cwtch', ); + + service.stream.listen((event) { + if (event is ToastDismissed) { + print('Toast was dismissed.'); + active = false; + } + if (event is ToastActivated) { + print('Toast was clicked.'); + active = false; + } + if (event is ToastInteracted) { + print('${event.action} action in the toast was clicked.'); + active = false; + } + }); } Future notify(String message) async { - Toast toast = new Toast( - type: ToastType.text01, - title: 'Cwtch', - subtitle: message, - ); - service.show(toast); + if (!globalAppState.focus) { + if (!active) { + Toast toast = new Toast( + type: ToastType.text02, + title: 'Cwtch', + subtitle: message, + ); + service.show(toast); + active = true; + } + } } } diff --git a/pubspec.lock b/pubspec.lock index 8f1137b3..1e1a4bec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -474,6 +474,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.4" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" xdg_directories: dependency: transitive description: From 04cf1e16c26a7d2428dc627fa9e5b2632e031478 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Wed, 26 Jan 2022 08:42:25 -0800 Subject: [PATCH 45/47] pubspec for windows_manager --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 532fffce..c4dc007b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: file_picker_desktop: ^1.1.0 url_launcher: ^6.0.12 desktoasts: ^0.0.2 + window_manager: ^0.1.4 dev_dependencies: msix: ^2.1.3 From dc587f95f05d1f6fa9abe77779e8e90ad6f60139 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Wed, 26 Jan 2022 08:48:35 -0800 Subject: [PATCH 46/47] remove prints, add comments --- lib/notification_manager.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart index aa7b84a1..c6463935 100644 --- a/lib/notification_manager.dart +++ b/lib/notification_manager.dart @@ -48,16 +48,16 @@ class WindowsNotificationManager implements NotificationsManager { ); service.stream.listen((event) { + // the user closed the notification of the OS timed it out if (event is ToastDismissed) { - print('Toast was dismissed.'); active = false; } + // clicked if (event is ToastActivated) { - print('Toast was clicked.'); - active = false; + active = false; } + // if a supplied action was clicked if (event is ToastInteracted) { - print('${event.action} action in the toast was clicked.'); active = false; } }); @@ -66,6 +66,8 @@ class WindowsNotificationManager implements NotificationsManager { Future notify(String message) async { if (!globalAppState.focus) { if (!active) { + // One string of bold text on the first line (title), + // one string (subtitle) of regular text wrapped across the second and third lines. Toast toast = new Toast( type: ToastType.text02, title: 'Cwtch', From 5c76628578d3e0b9e4663b603b4019dd6ff540bc Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 26 Jan 2022 12:25:03 -0800 Subject: [PATCH 47/47] Upgrade Cwtch and Display Message Limits --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- lib/notification_manager.dart | 3 +-- lib/views/messageview.dart | 16 +++++++++++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 124e8a8e..7d93107b 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-20-17-22-v1.5.4-16-ge0e1a4b \ No newline at end of file +2022-01-26-15-10-v1.5.4-18-gd77d7bb \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 3513fd2f..f05d8b3d 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-20-22-22-v1.5.4-16-ge0e1a4b \ No newline at end of file +2022-01-26-20-10-v1.5.4-18-gd77d7bb \ No newline at end of file diff --git a/lib/notification_manager.dart b/lib/notification_manager.dart index f01659be..72712b13 100644 --- a/lib/notification_manager.dart +++ b/lib/notification_manager.dart @@ -63,8 +63,7 @@ NotificationsManager newDesktopNotificationsManager() { client.getCapabilities(); return LinuxNotificationsManager(client); } catch (e) { - EnvironmentConfig.debugLog( - "Attempted to access DBUS for notifications but failed. Switching off notifications."); + EnvironmentConfig.debugLog("Attempted to access DBUS for notifications but failed. Switching off notifications."); } } else if (Platform.isWindows) { try { diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index ade71beb..9437dda8 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -175,8 +175,19 @@ class _MessageViewState extends State { )); } + // todo: legacy groups currently have restricted message + // size because of the additional wrapping end encoding + // hybrid groups should allow these numbers to be the same. + static const P2PMessageLengthMax = 7000; + static const GroupMessageLengthMax = 1800; + void _sendMessage([String? ignoredParam]) { - if (ctrlrCompose.value.text.isNotEmpty) { + var isGroup = Provider.of(context).contactList.getContact(Provider.of(context, listen: false).selectedConversation!)!.isGroup; + + // peers and groups currently have different length constraints (servers can store less)... + var lengthOk = (isGroup && ctrlrCompose.value.text.length < GroupMessageLengthMax) || ctrlrCompose.value.text.length <= P2PMessageLengthMax; + + if (ctrlrCompose.value.text.isNotEmpty && lengthOk) { if (Provider.of(context, listen: false).selectedConversation != null && Provider.of(context, listen: false).selectedIndex != null) { Provider.of(context, listen: false) .cwtch @@ -237,6 +248,7 @@ class _MessageViewState extends State { Widget _buildComposeBox() { bool isOffline = Provider.of(context).isOnline() == false; + bool isGroup = Provider.of(context).isGroup; var composeBox = Container( color: Provider.of(context).theme.backgroundMainColor, @@ -262,6 +274,8 @@ class _MessageViewState extends State { keyboardType: TextInputType.multiline, enableIMEPersonalizedLearning: false, minLines: 1, + maxLength: isGroup ? GroupMessageLengthMax : P2PMessageLengthMax, + maxLengthEnforcement: MaxLengthEnforcement.enforced, maxLines: null, onFieldSubmitted: _sendMessage, enabled: !isOffline,