From aeea5c9c8bc8aa28196c9237ea349dacf30e19b9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 13:25:16 +0200 Subject: [PATCH 01/29] fix performance issues --- .../visual/components/animate_icon.comp.dart | 5 +- .../visual/components/flame_counter.comp.dart | 1 + .../helpers/media_view_sizing.helper.dart | 9 +- .../visual/views/chats/chat_list.view.dart | 156 ++++---- .../group_list_item.comp.dart | 12 +- .../last_message_time.comp.dart | 15 +- .../typing_indicator_subtitle.comp.dart | 21 +- .../visual/views/chats/media_viewer.view.dart | 364 +++++++----------- .../emoji_reactions_row.comp.dart | 17 +- .../keyboard_dismiss_observer.comp.dart | 42 ++ .../media_viewer_bottom_navigation.comp.dart | 110 ++++++ .../reaction_buttons.comp.dart | 12 +- 12 files changed, 438 insertions(+), 326 deletions(-) create mode 100644 lib/src/visual/views/chats/media_viewer_components/keyboard_dismiss_observer.comp.dart create mode 100644 lib/src/visual/views/chats/media_viewer_components/media_viewer_bottom_navigation.comp.dart diff --git a/lib/src/visual/components/animate_icon.comp.dart b/lib/src/visual/components/animate_icon.comp.dart index bbc09956..5f721baf 100644 --- a/lib/src/visual/components/animate_icon.comp.dart +++ b/lib/src/visual/components/animate_icon.comp.dart @@ -4,7 +4,7 @@ import 'package:lottie/lottie.dart'; // animations from: https://googlefonts.github.io/noto-emoji-animation/ // from https://github.com/eitanliu/emoji_regex/tree/master -RegExp emojiRegex() => RegExp( +final RegExp _emojiRegex = RegExp( r'[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC08\uDC26](?:\u200D\u2B1B)?|[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)', ); @@ -12,7 +12,7 @@ bool isOneEmoji(String character) { if (EmojiAnimationComp.animatedIcons.containsKey(character)) { return true; } - final matches = emojiRegex().allMatches(character); + final matches = _emojiRegex.allMatches(character); if (matches.length == 1) { final match = matches.first; return match.start == 0 && match.end == character.length; @@ -28,6 +28,7 @@ class EmojiAnimationComp extends StatelessWidget { }); final String emoji; final bool repeat; + static final List animatedIconKeys = animatedIcons.keys.toList(); static final Map animatedIcons = { '❤': 'red_heart.lottie', '😂': 'joy.lottie', diff --git a/lib/src/visual/components/flame_counter.comp.dart b/lib/src/visual/components/flame_counter.comp.dart index 73d6491f..011a2877 100644 --- a/lib/src/visual/components/flame_counter.comp.dart +++ b/lib/src/visual/components/flame_counter.comp.dart @@ -98,6 +98,7 @@ class _FlameCounterWidgetState extends State { height: 11, child: EmojiAnimationComp( emoji: '⌛', + repeat: false, ), ), ], diff --git a/lib/src/visual/helpers/media_view_sizing.helper.dart b/lib/src/visual/helpers/media_view_sizing.helper.dart index 001f90cb..82f17d2a 100644 --- a/lib/src/visual/helpers/media_view_sizing.helper.dart +++ b/lib/src/visual/helpers/media_view_sizing.helper.dart @@ -22,15 +22,14 @@ class _MediaViewSizingHelperState extends State { @override Widget build(BuildContext context) { var needToDownSizeImage = false; - var availableHeight = MediaQuery.of(context).size.height; - // Get the screen size and safe area padding - final screenSize = MediaQuery.of(context).size; - final safeAreaPadding = MediaQuery.of(context).padding; + // Use narrow MediaQuery selectors to avoid rebuilding on keyboard inset changes + final screenSize = MediaQuery.sizeOf(context); + final safeAreaPadding = MediaQuery.paddingOf(context); // Calculate the available width and height final availableWidth = screenSize.width; - availableHeight = + final availableHeight = screenSize.height - safeAreaPadding.top - safeAreaPadding.bottom - diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 2b7e3f30..0605b307 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -38,7 +38,7 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie List _groupsPinned = []; List _groupsArchived = []; - bool _hasContacts = false; + final ValueNotifier _hasContacts = ValueNotifier(false); bool _loading = true; bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || @@ -50,6 +50,7 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie int _countContactRequest = 0; int _countAnnouncedUsers = 0; + final ValueNotifier _badgeCount = ValueNotifier(0); late StreamSubscription _countContactRequestStream; late StreamSubscription _countAnnouncedStream; @@ -80,9 +81,7 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie contacts, ) { if (!mounted) return; - setState(() { - _hasContacts = contacts.isNotEmpty; - }); + _hasContacts.value = contacts.isNotEmpty; }); _countContactRequestStream = twonlyDB.contactsDao @@ -90,9 +89,8 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie .listen((update) { if (update != null) { if (!mounted) return; - setState(() { - _countContactRequest = update; - }); + _countContactRequest = update; + _badgeCount.value = _countAnnouncedUsers + _countContactRequest; } }); @@ -100,9 +98,8 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie .watchNewAnnouncementsWithDataCount() .listen((update) { if (!mounted) return; - setState(() { - _countAnnouncedUsers = update; - }); + _countAnnouncedUsers = update; + _badgeCount.value = _countAnnouncedUsers + _countContactRequest; }); _precacheSub = twonlyDB.messagesDao.watchUnopenedMediaFiles().listen((mediaFiles) { @@ -141,7 +138,10 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie @override Widget build(BuildContext context) { super.build(context); - final plan = context.watch().plan; + final plan = context.select( + (p) => p.plan, + ); + final dark = isDarkMode(context); return Scaffold( appBar: AppBar( title: Row( @@ -175,7 +175,7 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, - color: isDarkMode(context) ? Colors.black : Colors.white, + color: dark ? Colors.black : Colors.white, ), ), ), @@ -184,40 +184,40 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie ), actions: [ const FeedbackIconButtonComp(), - Stack( - children: [ - if (_countAnnouncedUsers + _countContactRequest > 0) - Positioned.fill( - child: Center( - child: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: primaryColor, - shape: BoxShape.circle, + ValueListenableBuilder( + valueListenable: _badgeCount, + builder: (context, badgeCount, child) { + return Stack( + children: [ + if (badgeCount > 0) + Positioned.fill( + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + ), + Center( + child: NotificationBadgeComp( + backgroundColor: dark ? Colors.white : Colors.black, + textColor: dark ? Colors.black : Colors.white, + count: badgeCount.toString(), + child: IconButton( + color: badgeCount > 0 ? Colors.black : null, + key: searchForOtherUsers, + icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), + onPressed: () => context.push(Routes.chatsAddNewUser), ), ), ), - ), - Center( - child: NotificationBadgeComp( - backgroundColor: isDarkMode(context) - ? Colors.white - : Colors.black, - textColor: isDarkMode(context) ? Colors.black : Colors.white, - count: (_countAnnouncedUsers + _countContactRequest) - .toString(), - child: IconButton( - color: (_countAnnouncedUsers + _countContactRequest > 0) - ? Colors.black - : null, - key: searchForOtherUsers, - icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), - onPressed: () => context.push(Routes.chatsAddNewUser), - ), - ), - ), - ], + ], + ); + }, ), IconButton( @@ -303,45 +303,43 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie ), ), floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation, - floatingActionButton: !_hasContacts - ? null - : Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - heroTag: 'qrcode_fab', - elevation: 2, - backgroundColor: isDarkMode(context) - ? Colors.grey[800] - : Colors.grey[200], - foregroundColor: isDarkMode(context) - ? Colors.white - : Colors.black87, - onPressed: () => context.push(Routes.settingsPublicProfile), - child: FaIcon( - FontAwesomeIcons.qrcode, - color: isDarkMode(context) - ? Colors.white - : Colors.black87, - ), + floatingActionButton: ValueListenableBuilder( + valueListenable: _hasContacts, + builder: (context, hasContacts, child) { + if (!hasContacts) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.only(bottom: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: 'qrcode_fab', + elevation: 2, + backgroundColor: dark ? Colors.grey[800] : Colors.grey[200], + foregroundColor: dark ? Colors.white : Colors.black87, + onPressed: () => context.push(Routes.settingsPublicProfile), + child: FaIcon( + FontAwesomeIcons.qrcode, + color: dark ? Colors.white : Colors.black87, ), - const SizedBox(height: 12), - FloatingActionButton( - heroTag: 'new_chat_fab', - elevation: 2, - backgroundColor: primaryColor, - foregroundColor: Colors.black87, - onPressed: () => context.push(Routes.chatsStartNewChat), - child: const FaIcon( - FontAwesomeIcons.penToSquare, - color: Colors.black87, - ), + ), + const SizedBox(height: 12), + FloatingActionButton( + heroTag: 'new_chat_fab', + elevation: 2, + backgroundColor: primaryColor, + foregroundColor: Colors.black87, + onPressed: () => context.push(Routes.chatsStartNewChat), + child: const FaIcon( + FontAwesomeIcons.penToSquare, + color: Colors.black87, ), - ], - ), + ), + ], ), + ); + }, + ), ); } } diff --git a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart index 19cf9f51..eb10a7c3 100644 --- a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart @@ -174,19 +174,23 @@ class _UserListItem extends State { /// Fetches any media files referenced by preview messages but not yet in the /// local cache. Fire-and-forget; updates state when results arrive. Future _fetchMissingMediaFiles() async { + final missing = []; for (final message in _previewMessages) { if (message.mediaId != null && !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( message.mediaId!, ); - if (mediaFile != null && mounted) { - setState(() { - _previewMediaFiles.add(mediaFile); - }); + if (mediaFile != null) { + missing.add(mediaFile); } } } + if (missing.isNotEmpty && mounted) { + setState(() { + _previewMediaFiles.addAll(missing); + }); + } } Future onTap() async { diff --git a/lib/src/visual/views/chats/chat_list_components/last_message_time.comp.dart b/lib/src/visual/views/chats/chat_list_components/last_message_time.comp.dart index 7cefd901..3dd52298 100644 --- a/lib/src/visual/views/chats/chat_list_components/last_message_time.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/last_message_time.comp.dart @@ -26,11 +26,6 @@ class _LastMessageTimeCompState extends State { void initState() { super.initState(); _loadTargetTime(); - - updateTime = Timer.periodic( - const Duration(milliseconds: 500), - (_) => _updateSeconds(), - ); } @override @@ -66,6 +61,16 @@ class _LastMessageTimeCompState extends State { setState(() { lastMessageInSeconds = seconds < 0 ? 0 : seconds; }); + + var nextTickMs = 1000; + if (lastMessageInSeconds >= 120 && lastMessageInSeconds < 3600) { + nextTickMs = 30000; + } else if (lastMessageInSeconds >= 3600) { + nextTickMs = 60000; + } + + updateTime?.cancel(); + updateTime = Timer(Duration(milliseconds: nextTickMs), _updateSeconds); } @override diff --git a/lib/src/visual/views/chats/chat_list_components/typing_indicator_subtitle.comp.dart b/lib/src/visual/views/chats/chat_list_components/typing_indicator_subtitle.comp.dart index 40c38bcb..81397eaa 100644 --- a/lib/src/visual/views/chats/chat_list_components/typing_indicator_subtitle.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/typing_indicator_subtitle.comp.dart @@ -28,10 +28,6 @@ class _TypingIndicatorSubtitleCompState void initState() { super.initState(); - _periodicUpdate = Timer.periodic(const Duration(seconds: 1), (_) { - filterOpenUsers(_groupMembers); - }); - final membersStream = twonlyDB.groupsDao.watchGroupMembers( widget.groupId, ); @@ -41,11 +37,22 @@ class _TypingIndicatorSubtitleCompState } void filterOpenUsers(List input) { - if (mounted) { - setState(() { - _groupMembers = input.where(isTyping).toList(); + if (!mounted) return; + + final typingMembers = input.where(isTyping).toList(); + + if (typingMembers.isEmpty) { + _periodicUpdate?.cancel(); + _periodicUpdate = null; + } else { + _periodicUpdate ??= Timer.periodic(const Duration(seconds: 1), (_) { + filterOpenUsers(_groupMembers); }); } + + setState(() { + _groupMembers = typingMembers; + }); } @override diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 6e99ff8b..50916694 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -4,7 +4,6 @@ import 'dart:collection'; import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:mutex/mutex.dart'; import 'package:screen_protector/screen_protector.dart'; @@ -24,13 +23,13 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/animate_icon.comp.dart'; -import 'package:twonly/src/visual/elements/my_icon_button.element.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/chats/media_viewer_components/additional_message_content.dart'; +import 'package:twonly/src/visual/views/chats/media_viewer_components/keyboard_dismiss_observer.comp.dart'; import 'package:twonly/src/visual/views/chats/media_viewer_components/media_content_renderer.comp.dart'; +import 'package:twonly/src/visual/views/chats/media_viewer_components/media_viewer_bottom_navigation.comp.dart'; import 'package:twonly/src/visual/views/chats/media_viewer_components/media_viewer_message_input.comp.dart'; import 'package:twonly/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart'; import 'package:twonly/src/visual/views/chats/media_viewer_components/twonly_present_overlay.comp.dart'; @@ -63,7 +62,6 @@ class _MediaViewerViewState extends State { DateTime? canBeSeenUntil; final ValueNotifier progress = ValueNotifier(0); bool showSendTextMessageInput = false; - double maxBottomInset = 0; DateTime? _lastTimeInputClosed; final GlobalKey mediaWidgetKey = GlobalKey(); @@ -505,7 +503,7 @@ class _MediaViewerViewState extends State { advanceToNextMediaOrExit(); } }); - progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { + progressTimer = Timer.periodic(const Duration(milliseconds: 16), (timer) { final mediaFile = currentMedia?.mediaFile; if (mediaFile == null) return; if (mediaFile.displayLimitInMilliseconds == null || @@ -562,107 +560,49 @@ class _MediaViewerViewState extends State { } Widget bottomNavigation() { - return Row( + return MediaViewerBottomNavigationBar( key: mediaWidgetKey, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (currentMedia != null && - currentMessage != null && - !currentMedia!.mediaFile.requiresAuthentication && - currentMedia!.mediaFile.displayLimitInMilliseconds == null) - MyIconButton( - variant: MyIconButtonVariant.secondary, - onPressed: (currentMedia == null || currentMessage == null) - ? null - : onPressedSaveToGallery, - icon: imageSaving - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ) - : imageSaved - ? const Icon(Icons.check) - : const FaIcon(FontAwesomeIcons.floppyDisk, size: 20), + currentMedia: currentMedia, + currentMessage: currentMessage, + imageSaving: imageSaving, + imageSaved: imageSaved, + showShortReactions: showShortReactions, + onSaveToGallery: onPressedSaveToGallery, + onToggleReactions: () { + if (!showShortReactions) { + displayShortReactions(); + } else { + setState(() { + showShortReactions = false; + }); + } + }, + onMessagePressed: () { + displayShortReactions(); + setState(() { + showSendTextMessageInput = true; + }); + }, + onCameraPressed: () async { + nextMediaTimer?.cancel(); + progressTimer?.cancel(); + await videoController?.pause(); + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, ), - const SizedBox(width: 10), - IconButton( - icon: SizedBox( - width: 30, - height: 30, - child: GridView.count( - crossAxisCount: 2, - children: List.generate( - 4, - (index) { - return SizedBox( - width: 8, - height: 8, - child: Center( - child: EmojiAnimationComp( - emoji: EmojiAnimationComp.animatedIcons.keys - .toList()[index], - ), - ), - ); - }, - ), - ), - ), - onPressed: () { - if (!showShortReactions) { - displayShortReactions(); - } else { - setState(() { - showShortReactions = false; - }); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - ), - ), - ), - const SizedBox(width: 10), - MyIconButton( - variant: MyIconButtonVariant.secondary, - onPressed: () async { - displayShortReactions(); - setState(() { - showSendTextMessageInput = true; - }); - }, - icon: const FaIcon( - FontAwesomeIcons.message, - size: 20, - ), - ), - const SizedBox(width: 10), - MyIconButton( - onPressed: () async { - nextMediaTimer?.cancel(); - progressTimer?.cancel(); - await videoController?.pause(); - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - if (mounted && - currentMedia!.mediaFile.displayLimitInMilliseconds != null) { - await advanceToNextMediaOrExit(); - } else { - await videoController?.play(); - } - }, - icon: const FaIcon(FontAwesomeIcons.camera, size: 24), - ), - ], + ); + if (mounted && + currentMedia!.mediaFile.displayLimitInMilliseconds != null) { + await advanceToNextMediaOrExit(); + } else { + await videoController?.play(); + } + }, ); } @@ -717,124 +657,118 @@ class _MediaViewerViewState extends State { @override Widget build(BuildContext context) { - final bottomInset = MediaQuery.of(context).viewInsets.bottom; - if (bottomInset > maxBottomInset) { - maxBottomInset = bottomInset; - } else if (bottomInset == 0 && maxBottomInset > 0) { - maxBottomInset = 0; - if (showSendTextMessageInput) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - showSendTextMessageInput = false; - showShortReactions = false; - _lastTimeInputClosed = clock.now(); - }); - } + return KeyboardDismissObserver( + showSendTextMessageInput: showSendTextMessageInput, + onKeyboardDismissed: () { + setState(() { + showSendTextMessageInput = false; + showShortReactions = false; + _lastTimeInputClosed = clock.now(); }); - } - } - - return Scaffold( - body: SafeArea( - child: Stack( - fit: StackFit.expand, - children: [ - if (_showDownloadingLoader) _loader(), - if ((currentMedia != null || videoController != null) && - (canBeSeenUntil == null || progress.value >= 0)) - GestureDetector( - onTap: onScreenTapped, - onDoubleTap: (videoController == null) ? null : onScreenTapped, - child: MediaViewSizingHelper( - bottomNavigation: bottomNavigation(), - requiredHeight: 55, - child: MediaContentRenderer( - currentMedia: currentMedia, - videoController: videoController, - loader: _loader(), + }, + child: Scaffold( + body: SafeArea( + child: Stack( + fit: StackFit.expand, + children: [ + if (_showDownloadingLoader) _loader(), + if ((currentMedia != null || videoController != null) && + (canBeSeenUntil == null || progress.value >= 0)) + GestureDetector( + onTap: onScreenTapped, + onDoubleTap: (videoController == null) + ? null + : onScreenTapped, + child: MediaViewSizingHelper( + bottomNavigation: bottomNavigation(), + requiredHeight: 55, + child: MediaContentRenderer( + currentMedia: currentMedia, + videoController: videoController, + loader: _loader(), + ), + ), + ), + if (displayTwonlyPresent) + TwonlyPresentOverlay( + onTap: () => loadAndDownloadCurrentMedia(showTwonly: true), + ), + if (currentMedia != null && + currentMedia?.mediaFile.downloadState != DownloadState.ready) + Positioned.fill(child: _loader()), + if (canBeSeenUntil != null || progress.value >= 0) + Positioned( + right: 20, + top: 27, + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, child) { + return CircularProgressIndicator( + value: value, + strokeWidth: 2, + ); + }, + ), + ), + ], + ), + ), + Positioned( + top: 10, + left: showSendTextMessageInput ? 0 : null, + right: showSendTextMessageInput ? 0 : 15, + child: Text( + _currentMediaSender, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: showSendTextMessageInput ? 24 : 14, + fontWeight: FontWeight.bold, + color: showSendTextMessageInput + ? null + : const Color.fromARGB(255, 126, 126, 126), + shadows: const [ + Shadow( + color: Color.fromARGB(122, 0, 0, 0), + blurRadius: 5, + ), + ], ), ), ), - if (displayTwonlyPresent) - TwonlyPresentOverlay( - onTap: () => loadAndDownloadCurrentMedia(showTwonly: true), - ), - if (currentMedia != null && - currentMedia?.mediaFile.downloadState != DownloadState.ready) - Positioned.fill(child: _loader()), - if (canBeSeenUntil != null || progress.value >= 0) - Positioned( - right: 20, - top: 27, - child: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: ValueListenableBuilder( - valueListenable: progress, - builder: (context, value, child) { - return CircularProgressIndicator( - value: value, - strokeWidth: 2, - ); - }, - ), - ), - ], + if (showSendTextMessageInput) + MediaViewerMessageInput( + controller: textMessageController, + onSubmitted: (value) => _sendTextMessage(), + onSendPressed: _sendTextMessage, ), - ), - Positioned( - top: 10, - left: showSendTextMessageInput ? 0 : null, - right: showSendTextMessageInput ? 0 : 15, - child: Text( - _currentMediaSender, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: showSendTextMessageInput ? 24 : 14, - fontWeight: FontWeight.bold, - color: showSendTextMessageInput - ? null - : const Color.fromARGB(255, 126, 126, 126), - shadows: const [ - Shadow( - color: Color.fromARGB(122, 0, 0, 0), - blurRadius: 5, - ), - ], + if (currentMessage != null) + AdditionalMessageContent(currentMessage!), + if (currentMedia != null) + ReactionButtons( + show: showShortReactions, + textInputFocused: showSendTextMessageInput, + mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, + groupId: widget.group.groupId, + messageId: currentMessage!.messageId, + emojiKey: emojiKey, + hide: () { + setState(() { + showShortReactions = false; + showSendTextMessageInput = false; + _lastTimeInputClosed = clock.now(); + }); + }, ), + Positioned.fill( + child: EmojiFloatWidget(key: emojiKey), ), - ), - if (showSendTextMessageInput) - MediaViewerMessageInput( - controller: textMessageController, - onSubmitted: (value) => _sendTextMessage(), - onSendPressed: _sendTextMessage, - ), - if (currentMessage != null) - AdditionalMessageContent(currentMessage!), - if (currentMedia != null) - ReactionButtons( - show: showShortReactions, - textInputFocused: showSendTextMessageInput, - mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, - groupId: widget.group.groupId, - messageId: currentMessage!.messageId, - emojiKey: emojiKey, - hide: () { - setState(() { - showShortReactions = false; - showSendTextMessageInput = false; - _lastTimeInputClosed = clock.now(); - }); - }, - ), - Positioned.fill( - child: EmojiFloatWidget(key: emojiKey), - ), - ], + ], + ), ), ), ); diff --git a/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart b/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart index 4b5884a8..d76692f0 100644 --- a/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart +++ b/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart @@ -80,11 +80,18 @@ class _EmojiReactionWidgetState extends State { child: SizedBox( width: 40, child: Center( - child: widget.show - ? EmojiAnimationComp( - emoji: widget.emoji, - ) - : const SizedBox.shrink(), + child: AnimatedOpacity( + opacity: widget.show ? 1.0 : 0.0, + duration: widget.show + ? const Duration(milliseconds: 150) + : Duration.zero, + child: Offstage( + offstage: !widget.show, + child: EmojiAnimationComp( + emoji: widget.emoji, + ), + ), + ), ), ), ); diff --git a/lib/src/visual/views/chats/media_viewer_components/keyboard_dismiss_observer.comp.dart b/lib/src/visual/views/chats/media_viewer_components/keyboard_dismiss_observer.comp.dart new file mode 100644 index 00000000..afa9ec98 --- /dev/null +++ b/lib/src/visual/views/chats/media_viewer_components/keyboard_dismiss_observer.comp.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +/// Observes keyboard dismiss via viewInsets without causing the parent to +/// rebuild on every keyboard animation frame. +class KeyboardDismissObserver extends StatefulWidget { + const KeyboardDismissObserver({ + required this.showSendTextMessageInput, + required this.onKeyboardDismissed, + required this.child, + super.key, + }); + + final bool showSendTextMessageInput; + final VoidCallback onKeyboardDismissed; + final Widget child; + + @override + State createState() => + _KeyboardDismissObserverState(); +} + +class _KeyboardDismissObserverState extends State { + double _maxBottomInset = 0; + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.viewInsetsOf(context).bottom; + if (bottomInset > _maxBottomInset) { + _maxBottomInset = bottomInset; + } else if (bottomInset == 0 && _maxBottomInset > 0) { + _maxBottomInset = 0; + if (widget.showSendTextMessageInput) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + widget.onKeyboardDismissed(); + } + }); + } + } + return widget.child; + } +} diff --git a/lib/src/visual/views/chats/media_viewer_components/media_viewer_bottom_navigation.comp.dart b/lib/src/visual/views/chats/media_viewer_components/media_viewer_bottom_navigation.comp.dart new file mode 100644 index 00000000..45c0de41 --- /dev/null +++ b/lib/src/visual/views/chats/media_viewer_components/media_viewer_bottom_navigation.comp.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/visual/components/animate_icon.comp.dart'; +import 'package:twonly/src/visual/elements/my_icon_button.element.dart'; + +/// Extracted bottom navigation bar widget to provide subtree isolation. +/// Flutter can skip rebuilding this widget when only the parent's unrelated +/// state changes (e.g. progress timer, keyboard insets). +class MediaViewerBottomNavigationBar extends StatelessWidget { + const MediaViewerBottomNavigationBar({ + required this.currentMedia, + required this.currentMessage, + required this.imageSaving, + required this.imageSaved, + required this.showShortReactions, + required this.onSaveToGallery, + required this.onToggleReactions, + required this.onMessagePressed, + required this.onCameraPressed, + super.key, + }); + + final MediaFileService? currentMedia; + final Message? currentMessage; + final bool imageSaving; + final bool imageSaved; + final bool showShortReactions; + final VoidCallback onSaveToGallery; + final VoidCallback onToggleReactions; + final VoidCallback onMessagePressed; + final VoidCallback onCameraPressed; + + /// Pre-built static icon for the emoji grid button so the 4 Lottie + /// animations are created only once. + static final Widget _emojiGridIcon = SizedBox( + width: 30, + height: 30, + child: GridView.count( + crossAxisCount: 2, + children: List.generate( + 4, + (index) { + return SizedBox( + width: 8, + height: 8, + child: Center( + child: EmojiAnimationComp( + emoji: EmojiAnimationComp.animatedIconKeys[index], + ), + ), + ); + }, + ), + ), + ); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (currentMedia != null && + currentMessage != null && + !currentMedia!.mediaFile.requiresAuthentication && + currentMedia!.mediaFile.displayLimitInMilliseconds == null) + MyIconButton( + variant: MyIconButtonVariant.secondary, + onPressed: (currentMedia == null || currentMessage == null) + ? null + : onSaveToGallery, + icon: imageSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ) + : imageSaved + ? const Icon(Icons.check) + : const FaIcon(FontAwesomeIcons.floppyDisk, size: 20), + ), + const SizedBox(width: 10), + IconButton( + icon: _emojiGridIcon, + onPressed: onToggleReactions, + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + ), + ), + ), + const SizedBox(width: 10), + MyIconButton( + variant: MyIconButtonVariant.secondary, + onPressed: onMessagePressed, + icon: const FaIcon( + FontAwesomeIcons.message, + size: 20, + ), + ), + const SizedBox(width: 10), + MyIconButton( + onPressed: onCameraPressed, + icon: const FaIcon(FontAwesomeIcons.camera, size: 24), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart b/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart index 1aae402d..a1a18e33 100644 --- a/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart +++ b/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart @@ -335,9 +335,10 @@ class _ParticlePainter extends CustomPainter { _ParticlePainter(this.particles); final List<_Particle> particles; + static final _textPainter = TextPainter(textDirection: TextDirection.ltr); + @override void paint(Canvas canvas, Size size) { - final textPainter = TextPainter(textDirection: TextDirection.ltr); for (final p in particles) { final tp = TextSpan( text: p.emoji, @@ -350,14 +351,17 @@ class _ParticlePainter extends CustomPainter { : null, ), ); - textPainter + _textPainter ..text = tp ..layout(); canvas ..save() - ..translate(p.x - textPainter.width / 2, p.y - textPainter.height / 2) + ..translate( + p.x - _textPainter.width / 2, + p.y - _textPainter.height / 2, + ) ..rotate(p.rotation); - textPainter.paint(canvas, Offset.zero); + _textPainter.paint(canvas, Offset.zero); canvas.restore(); } } From d45ffbf597dbf98a6191311cb8ad6ea644b054f1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 16:20:53 +0200 Subject: [PATCH 02/29] make the passwordless backup visible --- CHANGELOG.md | 5 + lib/app.dart | 48 ++- .../generated/app_localizations.dart | 50 ++- .../generated/app_localizations_de.dart | 35 +- .../generated/app_localizations_en.dart | 36 +- lib/src/localization/translations | 2 +- lib/src/services/intent/links.intent.dart | 3 +- .../passwordless_recovery.service.dart | 6 - .../visual/elements/contact_chip.element.dart | 38 +- .../visual/views/chats/chat_list.view.dart | 2 + lib/src/visual/views/home.view.dart | 32 +- .../onboarding/recover_password.view.dart | 32 +- .../onboarding/recover_passwordless.view.dart | 42 ++- .../settings/backup/backup_settings.view.dart | 4 +- .../missing_recovery_contacts.comp.dart | 207 +++++++++++ .../components/second_factor_picker.comp.dart | 8 +- ...p_a_friend.passwordless_recovery.view.dart | 16 + .../settings.passwordless_recovery.view.dart | 329 +++++++++--------- .../setup.passwordless_recovery.view.dart | 15 +- pubspec.yaml | 2 +- 20 files changed, 626 insertions(+), 286 deletions(-) create mode 100644 lib/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 008bc369..e6772f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.4.0 + +- New: Passwordless Backup +- Fix: Performance issues + ## 0.3.7 - Fix: Multiple UI issues diff --git a/lib/app.dart b/lib/app.dart index 8a0dc086..ba3a4a3c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,7 +1,10 @@ import 'dart:async'; + +import 'package:app_links/app_links.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_sharing_intent/model/sharing_file.dart' show SharedFile; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; @@ -11,6 +14,8 @@ import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; +import 'package:twonly/src/services/intent/links.intent.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/pow.dart'; @@ -143,6 +148,10 @@ class _AppMainWidgetState extends State { bool _wasLogged = true; late int _initialPage; + StreamSubscription? _deepLinkSub; + StreamSubscription>? _intentStreamSub; + StreamSubscription? _emailTokenSub; + (Future?, bool) _proofOfWork = (null, false); @override @@ -151,6 +160,33 @@ class _AppMainWidgetState extends State { _initialPage = widget.initialPage; Log.info('AppWidgetState: initState started'); initAsync(); + + void handleShareLink(Uri uri) { + routerProvider.go(Routes.home); + HomeViewState.streamHomeViewPageIndex.add(1); + HomeViewState.streamSharedLink.add(uri); + } + + // Subscribe to all events (initial link and further) + _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { + if (!mounted) return; + Log.info('Got link via app links: ${uri.scheme}'); + if (!await handleIntentUrl(context, uri)) { + if (uri.scheme.startsWith('http')) { + handleShareLink(uri); + } + } + }); + + _intentStreamSub = initIntentStreams( + context, + handleShareLink, + ); + + _emailTokenSub = PasswordlessRecoveryService.onEmailTokenReceived.stream + .listen((token) { + routerProvider.go(Routes.recoverPasswordless, extra: token); + }); } Future initAsync() async { @@ -197,10 +233,6 @@ class _AppMainWidgetState extends State { } } - // await PasswordlessRecoveryService.handleRecoveryLink( - // 'https://me.twonly.eu/r/#7fdb8f08-0927-4e44-8761-038993414e48/1p7SKEzpxE3wSW9FQw60EUI4OpSW2U4EskdXLw8xg48', - // ); - setState(() { _isLoaded = true; }); @@ -264,4 +296,12 @@ class _AppMainWidgetState extends State { ], ); } + + @override + void dispose() { + _deepLinkSub?.cancel(); + _intentStreamSub?.cancel(); + _emailTokenSub?.cancel(); + super.dispose(); + } } diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 67645aec..10999072 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -3891,7 +3891,7 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryInfoHowItWorksDesc. /// /// In en, this message translates to: - /// **'Because twonly operates without central user accounts or phone numbers to maximize privacy, we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined threshold of these friends must combine their shares.'** + /// **'twonly operates without phone numbers, which is why we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined number of these friends must combine their shares.'** String get passwordlessRecoveryInfoHowItWorksDesc; /// No description provided for @passwordlessRecoveryInfoWhySecondFactor. @@ -3903,7 +3903,7 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryInfoWhySecondFactorDesc. /// /// In en, this message translates to: - /// **'The second factor (Email or PIN) serves as a vital cryptographic safeguard against malicious collusion. If your trusted friends were to coordinate their shares behind your back, they still wouldn\'t be able to decrypt your identity without the second factor key.'** + /// **'The second factor (Email or PIN) provides important additional protection. Even if your friends were to secretly team up, they still couldn\'t access your account without this second factor.'** String get passwordlessRecoveryInfoWhySecondFactorDesc; /// No description provided for @passwordlessRecoveryInfoGotIt. @@ -3966,6 +3966,18 @@ abstract class AppLocalizations { /// **'Select trusted friends'** String get passwordlessRecoverySelectFriends; + /// No description provided for @passwordlessRecoveryTrustedFriends. + /// + /// In en, this message translates to: + /// **'Trusted Friends'** + String get passwordlessRecoveryTrustedFriends; + + /// No description provided for @passwordlessRecoveryDoneBtn. + /// + /// In en, this message translates to: + /// **'Done ({count})'** + String passwordlessRecoveryDoneBtn(num count); + /// No description provided for @passwordlessRecoveryNoFriendsSelected. /// /// In en, this message translates to: @@ -4032,10 +4044,16 @@ abstract class AppLocalizations { /// **'No recovery share stored for this contact.'** String get passwordlessRecoveryNoShareStored; + /// No description provided for @passwordlessRecoveryEmailSent. + /// + /// In en, this message translates to: + /// **'Recovery email sent!'** + String get passwordlessRecoveryEmailSent; + /// No description provided for @passwordlessRecoveryShareSent. /// /// In en, this message translates to: - /// **'Recovery share successfully sent!'** + /// **'Recovery share sent!'** String get passwordlessRecoveryShareSent; /// No description provided for @passwordlessRecoveryNetworkError. @@ -4083,19 +4101,19 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryCantHelpHim. /// /// In en, this message translates to: - /// **'You can\'t help him'** + /// **'You can\'t help this person'** String get passwordlessRecoveryCantHelpHim; /// No description provided for @passwordlessRecoveryDoesAskedYou. /// /// In en, this message translates to: - /// **'Does {username} has asked you?'** + /// **'Has {username} asked you?'** String passwordlessRecoveryDoesAskedYou(Object username); /// No description provided for @passwordlessRecoveryVerifySourceDesc. /// /// In en, this message translates to: - /// **'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.'** + /// **'Please make sure this request really came from this person. If in doubt, ask them via another channel!'** String get passwordlessRecoveryVerifySourceDesc; /// No description provided for @passwordlessRecoveryNo. @@ -4125,7 +4143,7 @@ abstract class AppLocalizations { /// No description provided for @recoverPasswordlessSharesReceived. /// /// In en, this message translates to: - /// **'{received} of {threshold} friends have shared'** + /// **'{received} out of {threshold} friends have helped'** String recoverPasswordlessSharesReceived(Object received, Object threshold); /// No description provided for @recoverPasswordlessRecoverNowBtn. @@ -4133,6 +4151,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Recover now'** String get recoverPasswordlessRecoverNowBtn; + + /// No description provided for @missingRecoveryContactsCardTitle. + /// + /// In en, this message translates to: + /// **'Recovery Contacts'** + String get missingRecoveryContactsCardTitle; + + /// No description provided for @missingRecoveryContactsCardDesc. + /// + /// In en, this message translates to: + /// **'Select Recovery Contacts to help you recover your account without requiring a password.'** + String get missingRecoveryContactsCardDesc; + + /// No description provided for @missingRecoveryContactsCardAction. + /// + /// In en, this message translates to: + /// **'Select Contacts'** + String get missingRecoveryContactsCardAction; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 7f7046a2..f0daeed9 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2242,7 +2242,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoveryInfoHowItWorksDesc => - 'Da twonly ohne zentrale Benutzerkonten oder Telefonnummern funktioniert, um deine Privatsphäre zu maximieren, setzen wir auf einen dezentralen Wiederherstellungsmechanismus. Mit Shamir\'s Secret Sharing wird deine kryptografische Identität in unabhängige Teile aufgeteilt und an deine Freunde verteilt. Um den Zugriff wiederherzustellen, muss ein vorher festgelegter Schwellenwert dieser Freunde ihre Teile kombinieren.'; + 'twonly funktioniert ohne Telefonnummern, weshalb wir auf einen dezentralen Wiederherstellungsmechanismus setzen. Mit Shamir\'s Secret Sharing wird deine kryptografische Identität in unabhängige Teile aufgeteilt und an deine Freunde verteilt. Um den Zugriff wiederherzustellen, muss eine vorher festgelegte Anzahl dieser Freunde ihre Teile kombinieren.'; @override String get passwordlessRecoveryInfoWhySecondFactor => @@ -2250,7 +2250,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoveryInfoWhySecondFactorDesc => - 'Der zweite Faktor (E-Mail oder PIN) dient als wichtiger kryptografischer Schutz vor bösartiger Absprache. Wenn sich deine Freunde hinter deinem Rücken absprechen würden, könnten sie deine Identität ohne den Schlüssel für den zweiten Faktor trotzdem nicht entschlüsseln.'; + 'Der zweite Faktor (E-Mail oder PIN) ist ein wichtiger zusätzlicher Schutz. Selbst wenn sich deine Freunde heimlich zusammentun, können sie ohne diesen zweiten Faktor nicht auf dein Konto zugreifen.'; @override String get passwordlessRecoveryInfoGotIt => 'Verstanden'; @@ -2290,6 +2290,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoverySelectFriends => 'Freunde auswählen'; + @override + String get passwordlessRecoveryTrustedFriends => 'Vertrauenswürdige Freunde'; + + @override + String passwordlessRecoveryDoneBtn(num count) { + return 'Fertig ($count)'; + } + @override String get passwordlessRecoveryNoFriendsSelected => 'Noch keine Freunde ausgewählt'; @@ -2332,9 +2340,13 @@ class AppLocalizationsDe extends AppLocalizations { String get passwordlessRecoveryNoShareStored => 'Kein Wiederherstellungs-Teil für diesen Kontakt gespeichert.'; + @override + String get passwordlessRecoveryEmailSent => + 'Wiederherstellungs-E-Mail gesendet!'; + @override String get passwordlessRecoveryShareSent => - 'Wiederherstellungs-Teil erfolgreich gesendet!'; + 'Wiederherstellungs-Teil gesendet!'; @override String get passwordlessRecoveryNetworkError => @@ -2361,7 +2373,8 @@ class AppLocalizationsDe extends AppLocalizations { String get passwordlessRecoveryNoContactsFound => 'Keine Kontakte gefunden'; @override - String get passwordlessRecoveryCantHelpHim => 'Du kannst ihm nicht helfen'; + String get passwordlessRecoveryCantHelpHim => + 'Du kannst dieser Person nicht helfen'; @override String passwordlessRecoveryDoesAskedYou(Object username) { @@ -2370,7 +2383,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoveryVerifySourceDesc => - 'Bitte stelle sicher, dass du den Link/QR-Code tatsächlich von deinem Freund erhalten hast! Wenn du dir unsicher bist, verifiziere dies bitte erneut und klicke dann noch einmal auf den Link oder kehre zurück.'; + 'Bitte stelle sicher, dass diese Anfrage wirklich von dieser Person stammt. Im Zweifel frage kurz über einen anderen Kanal nach!'; @override String get passwordlessRecoveryNo => 'Nein'; @@ -2389,9 +2402,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String recoverPasswordlessSharesReceived(Object received, Object threshold) { - return '$received von $threshold Freunden haben geteilt'; + return '$received von $threshold Freunden haben geholfen'; } @override String get recoverPasswordlessRecoverNowBtn => 'Jetzt wiederherstellen'; + + @override + String get missingRecoveryContactsCardTitle => 'Vertrauenswürdige Kontakte'; + + @override + String get missingRecoveryContactsCardDesc => + 'Wähle Kontakte für die Kontowiederherstellung aus, die dir helfen, dein Konto ohne Passwort wiederherzustellen.'; + + @override + String get missingRecoveryContactsCardAction => 'Kontakte auswählen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 41a6b5eb..f0f3dc5e 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2226,14 +2226,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordlessRecoveryInfoHowItWorksDesc => - 'Because twonly operates without central user accounts or phone numbers to maximize privacy, we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined threshold of these friends must combine their shares.'; + 'twonly operates without phone numbers, which is why we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined number of these friends must combine their shares.'; @override String get passwordlessRecoveryInfoWhySecondFactor => 'Why a Second Factor?'; @override String get passwordlessRecoveryInfoWhySecondFactorDesc => - 'The second factor (Email or PIN) serves as a vital cryptographic safeguard against malicious collusion. If your trusted friends were to coordinate their shares behind your back, they still wouldn\'t be able to decrypt your identity without the second factor key.'; + 'The second factor (Email or PIN) provides important additional protection. Even if your friends were to secretly team up, they still couldn\'t access your account without this second factor.'; @override String get passwordlessRecoveryInfoGotIt => 'Got it'; @@ -2273,6 +2273,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordlessRecoverySelectFriends => 'Select trusted friends'; + @override + String get passwordlessRecoveryTrustedFriends => 'Trusted Friends'; + + @override + String passwordlessRecoveryDoneBtn(num count) { + return 'Done ($count)'; + } + @override String get passwordlessRecoveryNoFriendsSelected => 'No trusted friends selected yet'; @@ -2314,8 +2322,10 @@ class AppLocalizationsEn extends AppLocalizations { 'No recovery share stored for this contact.'; @override - String get passwordlessRecoveryShareSent => - 'Recovery share successfully sent!'; + String get passwordlessRecoveryEmailSent => 'Recovery email sent!'; + + @override + String get passwordlessRecoveryShareSent => 'Recovery share sent!'; @override String get passwordlessRecoveryNetworkError => @@ -2342,16 +2352,16 @@ class AppLocalizationsEn extends AppLocalizations { String get passwordlessRecoveryNoContactsFound => 'No contacts found'; @override - String get passwordlessRecoveryCantHelpHim => 'You can\'t help him'; + String get passwordlessRecoveryCantHelpHim => 'You can\'t help this person'; @override String passwordlessRecoveryDoesAskedYou(Object username) { - return 'Does $username has asked you?'; + return 'Has $username asked you?'; } @override String get passwordlessRecoveryVerifySourceDesc => - 'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.'; + 'Please make sure this request really came from this person. If in doubt, ask them via another channel!'; @override String get passwordlessRecoveryNo => 'No'; @@ -2370,9 +2380,19 @@ class AppLocalizationsEn extends AppLocalizations { @override String recoverPasswordlessSharesReceived(Object received, Object threshold) { - return '$received of $threshold friends have shared'; + return '$received out of $threshold friends have helped'; } @override String get recoverPasswordlessRecoverNowBtn => 'Recover now'; + + @override + String get missingRecoveryContactsCardTitle => 'Recovery Contacts'; + + @override + String get missingRecoveryContactsCardDesc => + 'Select Recovery Contacts to help you recover your account without requiring a password.'; + + @override + String get missingRecoveryContactsCardAction => 'Select Contacts'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index b7d703c8..2a54bd8e 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit b7d703c82a5ac0b6776847cb4eb6d637620f6585 +Subproject commit 2a54bd8e7f2021c5159c92b2e1dff8c8ed4effdc diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index c8042ebc..054397ec 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -32,8 +32,7 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { // Check if this is the QR code link which was // therefore scanned with the system camera - if (kDebugMode && - uri.toString().startsWith(PasswordlessRecoveryService.linkPrefix)) { + if (uri.toString().startsWith(PasswordlessRecoveryService.linkPrefix)) { await PasswordlessRecoveryService.handleRecoveryLink(uri.toString()); return true; } diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index 67e0e067..7770d119 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -8,12 +8,10 @@ import 'package:cryptography_plus/cryptography_plus.dart' show Hmac, Mac, SecretBox, SecretKey, Xchacha20; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:go_router/go_router.dart'; import 'package:twonly/core/bridge/wrapper.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart' show getContactDisplayName; import 'package:twonly/src/database/twonly.db.dart'; @@ -51,10 +49,6 @@ class PasswordlessRecoveryService { if (parts.length < 2) { if (fragment.isNotEmpty) { onEmailTokenReceived.add(fragment); - final context = rootNavigatorKey.currentContext; - if (context != null && context.mounted) { - unawaited(context.push(Routes.recoverPasswordless, extra: fragment)); - } } return; } diff --git a/lib/src/visual/elements/contact_chip.element.dart b/lib/src/visual/elements/contact_chip.element.dart index d6506396..f74356c1 100644 --- a/lib/src/visual/elements/contact_chip.element.dart +++ b/lib/src/visual/elements/contact_chip.element.dart @@ -10,49 +10,57 @@ import 'package:twonly/src/visual/views/contact/contact.view.dart'; class ContactChip extends StatelessWidget { const ContactChip({ - required this.contact, + this.contact, + this.username, this.onTap, super.key, - }); + }) : assert( + contact != null || username != null, + 'Either contact or username must be provided', + ); - final Contact contact; + final Contact? contact; + final String? username; final void Function(int)? onTap; @override Widget build(BuildContext context) { return ReactiveTapFeedback( onTap: () { + if (contact == null) return; if (onTap != null) { - onTap!(contact.userId); + onTap!(contact!.userId); } else { context.navPush( ContactView( - contact.userId, - key: ValueKey(contact.userId), + contact!.userId, + key: ValueKey(contact!.userId), ), ); } }, child: Chip( avatar: AvatarIcon( - contactId: contact.userId, + contactId: contact?.userId, fontSize: 10, ), label: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - getContactDisplayName(contact), + contact != null ? getContactDisplayName(contact!) : username!, style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 6), - VerificationBadgeComp( - contact: contact, - size: 12, - clickable: false, - ), - if (onTap != null) ...[ + if (contact != null) ...[ + const SizedBox(width: 6), + VerificationBadgeComp( + contact: contact, + size: 12, + clickable: false, + ), + ], + if (onTap != null && contact != null) ...[ const SizedBox(width: 15), const FaIcon( FontAwesomeIcons.xmark, diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 0605b307..58de7f31 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -21,6 +21,7 @@ import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn. import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart'; +import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart'; class ChatListView extends StatefulWidget { const ChatListView({super.key}); @@ -236,6 +237,7 @@ class _ChatListViewState extends State with AutomaticKeepAliveClie children: [ const FinishSetupComp(), const MissingBackupComp(), + const MissingRecoveryContactsComp(), if (_loading) const Expanded( child: SizedBox.shrink(), diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index f6cfdcba..c85eb538 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -1,18 +1,15 @@ import 'dart:async'; -import 'package:app_links/app_links.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/providers/routing.provider.dart'; -import 'package:twonly/src/services/intent/links.intent.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; @@ -46,13 +43,13 @@ class HomeViewState extends State with WidgetsBindingObserver { final MainCameraController _mainCameraController = MainCameraController(); late final PageController _homeViewPageController; - StreamSubscription>? _intentStreamSub; - StreamSubscription? _deepLinkSub; + StreamSubscription? _sharedLinkSub; StreamSubscription? _onMessageOpenedAppSub; StreamSubscription? _homeViewPageIndexSub; StreamSubscription? _selectNotificationSub; static final streamHomeViewPageIndex = StreamController.broadcast(); + static final streamSharedLink = StreamController.broadcast(); @override void initState() { @@ -113,26 +110,8 @@ class HomeViewState extends State with WidgetsBindingObserver { unawaited(_initAsync()); - void handleShareLink(Uri uri) { - routerProvider.go(Routes.home); - streamHomeViewPageIndex.add(1); - _mainCameraController.setSharedLinkForPreview(uri); - } - - // Subscribe to all events (initial link and further) - _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { - if (!mounted) return; - Log.info('Got link via app links: ${uri.scheme}'); - if (!await handleIntentUrl(context, uri)) { - if (uri.scheme.startsWith('http')) { - handleShareLink(uri); - } - } - }); - - _intentStreamSub = initIntentStreams( - context, - handleShareLink, + _sharedLinkSub = streamSharedLink.stream.listen( + _mainCameraController.setSharedLinkForPreview, ); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -207,8 +186,7 @@ class HomeViewState extends State with WidgetsBindingObserver { _disableCameraTimer?.cancel(); _mainCameraController.setState = null; _mainCameraController.closeCamera(); - _intentStreamSub?.cancel(); - _deepLinkSub?.cancel(); + _sharedLinkSub?.cancel(); super.dispose(); } diff --git a/lib/src/visual/views/onboarding/recover_password.view.dart b/lib/src/visual/views/onboarding/recover_password.view.dart index 425c9271..205949f5 100644 --- a/lib/src/visual/views/onboarding/recover_password.view.dart +++ b/lib/src/visual/views/onboarding/recover_password.view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; @@ -168,23 +167,22 @@ class _BackupRecoveryViewState extends State { : Text(context.lang.twonlySafeRecoverBtn), ), const SizedBox(height: 16), - if (kDebugMode) - MyButton( - variant: MyButtonVariant.secondary, - onPressed: () async { - await KeyValueStore.update( - key: KeyValueKeys.onboardingState, - update: (state) => - state.hasStartedPasswordlessRecovery = true, - ); - if (context.mounted) { - await context.push(Routes.recoverPasswordless); - } - }, - child: Text( - context.lang.passwordlessRecoveryRecoverBtn, - ), + MyButton( + variant: MyButtonVariant.secondary, + onPressed: () async { + await KeyValueStore.update( + key: KeyValueKeys.onboardingState, + update: (state) => + state.hasStartedPasswordlessRecovery = true, + ); + if (context.mounted) { + await context.push(Routes.recoverPasswordless); + } + }, + child: Text( + context.lang.passwordlessRecoveryRecoverBtn, ), + ), const Spacer(), const SizedBox(height: 40), ], diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart index 78bb8fc9..f8b88a1b 100644 --- a/lib/src/visual/views/onboarding/recover_passwordless.view.dart +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -26,6 +26,7 @@ import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/elements/contact_chip.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_input.element.dart'; import 'package:twonly/src/visual/views/onboarding/components/animated_bell_icon.comp.dart'; @@ -337,7 +338,7 @@ class _RecoverPasswordlessState extends State { if (mounted) { showSnackbar( context, - context.lang.passwordlessRecoveryShareSent, + context.lang.passwordlessRecoveryEmailSent, level: SnackbarLevel.success, ); } @@ -511,29 +512,26 @@ class _RecoverPasswordlessState extends State { const SizedBox(height: 24), Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( context.lang.recoverPasswordlessSharesReceived( shares.length, threshold, ), - style: Theme.of(context).textTheme.bodyMedium, - ), - const Spacer(), - if (thresholdReached) - const Icon( - Icons.check_circle_rounded, - color: Colors.green, - size: 20, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, ), + textAlign: TextAlign.center, + ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 12), ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), child: LinearProgressIndicator( value: shares.length / threshold, - minHeight: 8, + minHeight: 12, backgroundColor: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.08), @@ -542,16 +540,16 @@ class _RecoverPasswordlessState extends State { ), ), ), - const SizedBox(height: 16), - - ...shares.map( - (share) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - share.trustedFriendDisplayName, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), + const SizedBox(height: 24), + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.center, + children: shares.map((share) { + return ContactChip( + username: share.trustedFriendDisplayName, + ); + }).toList(), ), const SizedBox(height: 24), diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index 54c0e749..2f381f37 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; @@ -186,8 +185,7 @@ class _BackupViewState extends State { ], ), - if (userService.currentUser.passwordLessRecovery == null && - kDebugMode) ...[ + if (userService.currentUser.passwordLessRecovery == null) ...[ const SizedBox(height: 20), Center( child: MyButton( diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart new file mode 100644 index 00000000..c9483ea1 --- /dev/null +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart @@ -0,0 +1,207 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart'; + +class MissingRecoveryContactsComp extends StatefulWidget { + const MissingRecoveryContactsComp({super.key}); + + @override + State createState() => + _MissingRecoveryContactsCompState(); +} + +class _MissingRecoveryContactsCompState + extends State { + bool _hasEnoughFriends = false; + bool _loading = true; + StreamSubscription>? _contactsSub; + + @override + void initState() { + super.initState(); + _checkFriends(); + _contactsSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((_) { + _checkFriends(); + }); + } + + @override + void dispose() { + _contactsSub?.cancel(); + super.dispose(); + } + + Future _checkFriends() async { + final user = userService.currentUser; + if (user.currentSetupPage != null || user.passwordLessRecovery != null) { + if (mounted) { + setState(() { + _loading = false; + }); + } + return; + } + + final kvs = await twonlyDB.select(twonlyDB.keyVerifications).get(); + final urs = await (twonlyDB.select( + twonlyDB.userDiscoveryUserRelations, + )..where((u) => u.publicKeyVerifiedTimestamp.isNotNull())).get(); + + final verified = { + ...kvs.map((row) => row.contactId), + ...urs.map((row) => row.announcedUserId), + }; + + final contacts = await twonlyDB.contactsDao.getAllContacts(); + var count = 0; + for (final c in contacts) { + if (verified.contains(c.userId) && + c.accepted && + !c.blocked && + !c.accountDeleted && + !c.deletedByUser) { + count++; + } + } + + if (mounted) { + setState(() { + _hasEnoughFriends = count >= 4; + _loading = false; + }); + } + } + + Future onTap() async { + await context.navPush(const PasswordLessRecoverySetup()); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + + if (user.currentSetupPage != null || + user.passwordLessRecovery != null) { + return const SizedBox.shrink(); + } + + if (_loading || !_hasEnoughFriends) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + context.color.secondaryContainer.withValues(alpha: 0.2), + context.color.secondaryContainer.withValues(alpha: 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + border: Border.all( + color: context.color.secondary.withValues(alpha: 0.15), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: context.color.shadow.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(24), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + SizedBox( + width: 68, + height: 68, + child: Container( + decoration: BoxDecoration( + color: context.color.secondary.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Center( + child: FaIcon( + FontAwesomeIcons.users, + size: 28, + color: context.color.secondary, + ), + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.missingRecoveryContactsCardTitle, + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 16, + color: context.color.onSurface, + letterSpacing: -0.2, + ), + ), + const SizedBox(height: 6), + Text( + context.lang.missingRecoveryContactsCardDesc, + style: TextStyle( + fontSize: 13, + color: context.color.onSurfaceVariant, + height: 1.3, + ), + ), + const SizedBox(height: 14), + MyButton( + onPressed: onTap, + variant: MyButtonVariant.primaryDense, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.arrow_forward_rounded, + size: 18, + ), + const SizedBox(width: 8), + Text( + context + .lang + .missingRecoveryContactsCardAction, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/components/second_factor_picker.comp.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/components/second_factor_picker.comp.dart index 24dcf175..39b0582c 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/components/second_factor_picker.comp.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/components/second_factor_picker.comp.dart @@ -17,16 +17,16 @@ class _FactorOption { const _options = [ _FactorOption( - type: SecondFactorType.none, - icon: Icons.block_rounded, + type: SecondFactorType.email, + icon: Icons.email_rounded, ), _FactorOption( type: SecondFactorType.pin, icon: Icons.dialpad_rounded, ), _FactorOption( - type: SecondFactorType.email, - icon: Icons.email_rounded, + type: SecondFactorType.none, + icon: Icons.block_rounded, ), ]; diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart index 9f4caa80..12c0adeb 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart @@ -285,6 +285,22 @@ class _ConfirmRecoveryDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Row( + children: [ + AvatarIcon(contactId: widget.contact.userId), + const SizedBox(width: 12), + Expanded( + child: Text( + getContactDisplayName(widget.contact), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ], + ), + const SizedBox(height: 16), Text( context.lang.passwordlessRecoveryVerifySourceDesc, style: TextStyle( diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart index f03b2bbe..9afe306a 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart @@ -93,172 +93,187 @@ class PasswordLessRecoverySettings extends StatelessWidget { @override Widget build(BuildContext context) { - final config = userService.currentUser.passwordLessRecovery; + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, _) { + final config = userService.currentUser.passwordLessRecovery; - if (config == null) { - return Scaffold( - appBar: AppBar(title: Text(context.lang.passwordlessRecovery)), - body: Center( - child: Text(context.lang.passwordlessRecoveryNotConfigured), - ), - ); - } - - var secondFactorLabel = context.lang.passwordlessRecoverySecondFactorNone; - var secondFactorIcon = Icons.lock_outline_rounded; - Widget? actionButton; - - if (config.email != null) { - secondFactorLabel = context.lang - .passwordlessRecoverySecondFactorEmailLabel(config.email!); - secondFactorIcon = Icons.email_outlined; - } else if (config.pinSeed != null) { - secondFactorLabel = context.lang.passwordlessRecoverySecondFactorPin; - secondFactorIcon = Icons.pin_outlined; - actionButton = MyButton( - variant: MyButtonVariant.secondaryDense, - onPressed: () => _testPin(context), - child: Text(context.lang.passwordlessRecoveryTestPin), - ); - } - - return Scaffold( - appBar: AppBar( - title: Text(context.lang.passwordlessRecovery), - actions: [ - IconButton( - onPressed: () => showPasswordlessRecoveryInfoSheet(context), - icon: const Icon(Icons.info_outline_rounded), - ), - ], - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - MyButton( - variant: MyButtonVariant.secondary, - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const PasswordLessRecoverySetup(), - ), + if (config == null) { + return Scaffold( + appBar: AppBar(title: Text(context.lang.passwordlessRecovery)), + body: Center( + child: Text(context.lang.passwordlessRecoveryNotConfigured), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + ); + } + + var secondFactorLabel = + context.lang.passwordlessRecoverySecondFactorNone; + var secondFactorIcon = Icons.lock_outline_rounded; + Widget? actionButton; + + if (config.email != null) { + secondFactorLabel = context.lang + .passwordlessRecoverySecondFactorEmailLabel(config.email!); + secondFactorIcon = Icons.email_outlined; + } else if (config.pinSeed != null) { + secondFactorLabel = context.lang.passwordlessRecoverySecondFactorPin; + secondFactorIcon = Icons.pin_outlined; + actionButton = MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: () => _testPin(context), + child: Text(context.lang.passwordlessRecoveryTestPin), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text(context.lang.passwordlessRecovery), + actions: [ + IconButton( + onPressed: () => showPasswordlessRecoveryInfoSheet(context), + icon: const Icon(Icons.info_outline_rounded), + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + MyButton( + variant: MyButtonVariant.secondary, + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const PasswordLessRecoverySetup(), + ), + ), + child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.manage_accounts_rounded, size: 18), - const SizedBox(width: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.manage_accounts_rounded, size: 18), + const SizedBox(width: 8), + Text( + context.lang.passwordlessRecoveryModify, + style: const TextStyle(fontSize: 14), + ), + ], + ), + const SizedBox(height: 2), Text( - context.lang.passwordlessRecoveryModify, - style: const TextStyle(fontSize: 14), + context.lang.passwordlessRecoveryModifyDesc, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: context.color.onSurfaceVariant, + fontSize: 12, + ), + textAlign: TextAlign.center, ), ], ), - const SizedBox(height: 2), - Text( - context.lang.passwordlessRecoveryModifyDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: context.color.onSurfaceVariant, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - ), + ), + const SizedBox(height: 24), + _buildInfoCard( + context, + title: context.lang.passwordlessRecoverySecondFactor, + value: secondFactorLabel, + icon: secondFactorIcon, + action: actionButton, + ), + const SizedBox(height: 24), + StreamBuilder>( + stream: + (twonlyDB.select( + twonlyDB.contacts, + )..where((t) => t.recoveryIsTrustedFriend.equals(true))) + .watch(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + + final friends = snapshot.data!; + if (friends.isEmpty) { + return Text( + context.lang.passwordlessRecoveryNoFriendsFound, + ); + } + + final activeFriends = friends.where((c) { + final lastHeartbeat = c.recoveryLastHeartbeat; + if (lastHeartbeat == null) return false; + return DateTime.now().difference(lastHeartbeat).inDays <= + 14; + }).toList(); + + final inactiveFriends = friends.where((c) { + final lastHeartbeat = c.recoveryLastHeartbeat; + if (lastHeartbeat == null) return true; + return DateTime.now().difference(lastHeartbeat).inDays > 14; + }).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (activeFriends.isNotEmpty) ...[ + Text( + context.lang.passwordlessRecoveryActiveFriends, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + context.lang.passwordlessRecoveryActiveFriendsDesc, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + _buildFriendsGridCard( + context, + friends: activeFriends, + isActive: true, + ), + const SizedBox(height: 24), + ], + if (inactiveFriends.isNotEmpty) ...[ + Text( + context.lang.passwordlessRecoveryInactiveFriends, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + context.lang.passwordlessRecoveryInactiveFriendsDesc, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + _buildFriendsGridCard( + context, + friends: inactiveFriends, + isActive: false, + ), + ], + ], + ); + }, + ), + ], ), - const SizedBox(height: 24), - _buildInfoCard( - context, - title: context.lang.passwordlessRecoverySecondFactor, - value: secondFactorLabel, - icon: secondFactorIcon, - action: actionButton, - ), - const SizedBox(height: 24), - StreamBuilder>( - stream: (twonlyDB.select( - twonlyDB.contacts, - )..where((t) => t.recoveryIsTrustedFriend.equals(true))).watch(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - - final friends = snapshot.data!; - if (friends.isEmpty) { - return Text(context.lang.passwordlessRecoveryNoFriendsFound); - } - - final activeFriends = friends.where((c) { - final lastHeartbeat = c.recoveryLastHeartbeat; - if (lastHeartbeat == null) return false; - return DateTime.now().difference(lastHeartbeat).inDays <= 14; - }).toList(); - - final inactiveFriends = friends.where((c) { - final lastHeartbeat = c.recoveryLastHeartbeat; - if (lastHeartbeat == null) return true; - return DateTime.now().difference(lastHeartbeat).inDays > 14; - }).toList(); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (activeFriends.isNotEmpty) ...[ - Text( - context.lang.passwordlessRecoveryActiveFriends, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - context.lang.passwordlessRecoveryActiveFriendsDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: context.color.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - _buildFriendsGridCard( - context, - friends: activeFriends, - isActive: true, - ), - const SizedBox(height: 24), - ], - if (inactiveFriends.isNotEmpty) ...[ - Text( - context.lang.passwordlessRecoveryInactiveFriends, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - context.lang.passwordlessRecoveryInactiveFriendsDesc, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: context.color.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - _buildFriendsGridCard( - context, - friends: inactiveFriends, - isActive: false, - ), - ], - ], - ); - }, - ), - ], - ), + ); + }, ); } diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart index 669694ee..5d5b5504 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart @@ -69,12 +69,14 @@ class _PasswordLessRecoverySetupState extends State { if (config.email != null) { _secondFactor = SecondFactorType.email; _emailController.text = config.email!; + _threshold = 2; } else if (config.pinSeed != null) { _secondFactor = SecondFactorType.pin; + _threshold = 2; } else { _secondFactor = SecondFactorType.none; + _threshold = 4; } - _threshold = max(2, (selectedContacts.length / 2).ceil()); }); } else { contacts.sortBy((c) => c.mediaSendCounter); @@ -137,8 +139,9 @@ class _PasswordLessRecoverySetupState extends State { MaterialPageRoute( builder: (_) => SelectContactsView( text: SelectedContactView( - title: 'Trusted Friends', - submitButton: (selected, _) => 'Done ($selected)', + title: context.lang.passwordlessRecoveryTrustedFriends, + submitButton: (selected, _) => + context.lang.passwordlessRecoveryDoneBtn(selected), submitIcon: FontAwesomeIcons.check, ), alreadySelected: _selectedContacts.map((c) => c.userId).toList(), @@ -230,7 +233,7 @@ class _PasswordLessRecoverySetupState extends State { onSelectFriends: _selectTrustedFriends, onRemoveContact: (userId) => setState(() { _selectedContacts.removeWhere((c) => c.userId == userId); - _threshold = _validThreshold; + _threshold = _minThreshold; }), ), const SizedBox(height: 28), @@ -239,7 +242,7 @@ class _PasswordLessRecoverySetupState extends State { selected: _secondFactor, onChanged: (type) => setState(() { _secondFactor = type; - _threshold = _validThreshold; + _threshold = _minThreshold; }), pinController: _pinController, emailController: _emailController, @@ -250,7 +253,7 @@ class _PasswordLessRecoverySetupState extends State { ThresholdPicker( contactCount: _selectedContacts.length, minThreshold: _minThreshold, - currentThreshold: _validThreshold, + currentThreshold: _threshold, onChanged: (value) => setState(() => _threshold = value), ), const SizedBox(height: 28), diff --git a/pubspec.yaml b/pubspec.yaml index d55ded6c..53d91f4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.3.7+153 +version: 0.4.0+154 environment: sdk: ^3.11.0 From 59bdfa4eef0710a8de5d338f14f5f3d86bdc9b60 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 19:05:00 +0200 Subject: [PATCH 03/29] sort users --- .../setup.passwordless_recovery.view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart index 5d5b5504..f1bdcd9e 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -79,7 +78,7 @@ class _PasswordLessRecoverySetupState extends State { } }); } else { - contacts.sortBy((c) => c.mediaSendCounter); + contacts.sort((a, b) => b.mediaSendCounter.compareTo(a.mediaSendCounter)); final verifiedContacts = contacts .where( (c) => From fe673c9e23ddd292f55131911d4b616619cdc78d Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 19:05:15 +0200 Subject: [PATCH 04/29] update lottie --- dependencies | 2 +- .../settings/chat/chat_reactions.view.dart | 22 ++++++++++--------- pubspec.yaml | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/dependencies b/dependencies index 72d9bd63..d99dba6a 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit 72d9bd6320bca1f1d29c6e61c3821fed326c0abe +Subproject commit d99dba6a5e0eb13d7dcb38a842e64c23a1779f1c diff --git a/lib/src/visual/views/settings/chat/chat_reactions.view.dart b/lib/src/visual/views/settings/chat/chat_reactions.view.dart index 19707b0b..d050baa4 100644 --- a/lib/src/visual/views/settings/chat/chat_reactions.view.dart +++ b/lib/src/visual/views/settings/chat/chat_reactions.view.dart @@ -16,12 +16,16 @@ class ChatReactionSelectionView extends StatefulWidget { class _ChatReactionSelectionView extends State { List _selectedEmojis = []; + /// Cache the emoji keys list once to avoid repeated Map.keys.toList() calls. + static final List _allEmojis = EmojiAnimationComp.animatedIcons.keys + .toList(); + List _emojisFromSession() { final user = userService.currentUser; if (user.preSelectedEmojies != null) { return user.preSelectedEmojies!; } - return EmojiAnimationComp.animatedIcons.keys.toList().sublist(0, 6); + return _allEmojis.sublist(0, 6); } @override @@ -60,22 +64,20 @@ class _ChatReactionSelectionView extends State { gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, ), - itemCount: EmojiAnimationComp.animatedIcons.keys.length, + itemCount: _allEmojis.length, itemBuilder: (context, index) { - final emoji = EmojiAnimationComp.animatedIcons.keys.elementAt( - index, - ); + final emoji = _allEmojis[index]; + final isSelected = _selectedEmojis.contains(emoji); return GestureDetector( onTap: () => _onEmojiSelected(emoji), child: Card( - color: _selectedEmojis.contains(emoji) + color: isSelected ? context.color.primary.withAlpha(150) : context.color.surface, child: Center( - child: SizedBox( - width: 40, - height: 40, - child: EmojiAnimationComp(emoji: emoji), + child: Text( + emoji, + style: const TextStyle(fontSize: 32), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 53d91f4b..8a1dfc45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.4.0+154 +version: 0.4.0+155 environment: sdk: ^3.11.0 From 2a4574ef36fafa335e576bafce70b91cdca72675 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 20:18:35 +0200 Subject: [PATCH 05/29] allow duplicate import --- .../import_from_gallery.view.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart b/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart index 7b7b6848..6755eace 100644 --- a/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart @@ -87,7 +87,11 @@ class _ImportFromGalleryViewState extends State { final exists = await twonlyDB.mediaFilesDao.getMediaByHash(hash); if (exists.isNotEmpty) { - duplicated += 1; + final mediaFile = exists.first; + final mediaService = MediaFileService(mediaFile); + await mediaService.storedPath.parent.create(recursive: true); + await File(mediaService.storedPath.path).writeAsBytes(bytes); + importedCount++; continue; } @@ -371,13 +375,17 @@ class _ImportFromGalleryViewState extends State { try { final assetName = await asset.titleAsync; final dotIndex = assetName.lastIndexOf('.'); - final baseName = dotIndex != -1 ? assetName.substring(0, dotIndex) : assetName; + final baseName = dotIndex != -1 + ? assetName.substring(0, dotIndex) + : assetName; final uuidRegex = RegExp( r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', ); if (uuidRegex.hasMatch(baseName)) { - final existing = await twonlyDB.mediaFilesDao.getMediaFileById(baseName); + final existing = await twonlyDB.mediaFilesDao.getMediaFileById( + baseName, + ); if (existing != null) { final mediaService = MediaFileService(existing); if (!mediaService.storedPath.existsSync()) { @@ -388,7 +396,9 @@ class _ImportFromGalleryViewState extends State { stored: const Value(true), ), ); - mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(baseName); + mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( + baseName, + ); isRestored = true; } } From 4588c9995dce35db3ebbc344d467a0ad23953c39 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 20:20:02 +0200 Subject: [PATCH 06/29] fix some smaller issues --- lib/app.dart | 6 ------ lib/src/model/json/userdata.model.dart | 5 +++++ lib/src/model/json/userdata.model.g.dart | 14 ++++++++------ .../passwordless_recovery.service.dart | 3 +++ .../visual/components/animate_icon.comp.dart | 2 +- .../onboarding/recover_passwordless.view.dart | 19 +++++++------------ .../settings/chat/chat_reactions.view.dart | 14 ++++++++++---- pubspec.lock | 2 +- 8 files changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index ba3a4a3c..c65fa7c2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -15,7 +15,6 @@ import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/intent/links.intent.dart'; -import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/pow.dart'; @@ -182,11 +181,6 @@ class _AppMainWidgetState extends State { context, handleShareLink, ); - - _emailTokenSub = PasswordlessRecoveryService.onEmailTokenReceived.stream - .listen((token) { - routerProvider.go(Routes.recoverPasswordless, extra: token); - }); } Future initAsync() async { diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index e16ca159..cf245035 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -195,8 +195,12 @@ class TwonlySafeBackup { factory TwonlySafeBackup.fromJson(Map json) => _$TwonlySafeBackupFromJson(json); + @JsonKey(defaultValue: 0) int lastBackupSize = 0; + + @JsonKey(defaultValue: LastBackupUploadState.none) LastBackupUploadState backupUploadState = LastBackupUploadState.none; + DateTime? lastBackupDone; List backupId; List encryptionKey; @@ -216,6 +220,7 @@ class PasswordLessRecovery { // <-- // Data shared with trusted friends + @JsonKey(defaultValue: 2) int threshold; // Trusted friends are able to brute-force the pin -> Server delets after X tries List? pinSeed; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index e92748ef..13e3565a 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -202,11 +202,13 @@ TwonlySafeBackup _$TwonlySafeBackupFromJson(Map json) => .map((e) => (e as num).toInt()) .toList(), ) - ..lastBackupSize = (json['lastBackupSize'] as num).toInt() - ..backupUploadState = $enumDecode( - _$LastBackupUploadStateEnumMap, - json['backupUploadState'], - ) + ..lastBackupSize = (json['lastBackupSize'] as num?)?.toInt() ?? 0 + ..backupUploadState = + $enumDecodeNullable( + _$LastBackupUploadStateEnumMap, + json['backupUploadState'], + ) ?? + LastBackupUploadState.none ..lastBackupDone = json['lastBackupDone'] == null ? null : DateTime.parse(json['lastBackupDone'] as String); @@ -230,7 +232,7 @@ const _$LastBackupUploadStateEnumMap = { PasswordLessRecovery _$PasswordLessRecoveryFromJson( Map json, -) => PasswordLessRecovery((json['threshold'] as num).toInt()) +) => PasswordLessRecovery((json['threshold'] as num?)?.toInt() ?? 2) ..email = json['email'] as String? ..pinSeed = (json['pinSeed'] as List?) ?.map((e) => (e as num).toInt()) diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index 7770d119..ced9abd4 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -36,6 +36,8 @@ class PasswordlessRecoveryService { static final StreamController onEmailTokenReceived = StreamController.broadcast(); + static String? lastEmailToken; + static String linkPrefix = 'https://me.twonly.eu/r/#'; static final Set _handledNotificationIds = {}; @@ -48,6 +50,7 @@ class PasswordlessRecoveryService { final parts = fragment.split('/'); if (parts.length < 2) { if (fragment.isNotEmpty) { + lastEmailToken = fragment; onEmailTokenReceived.add(fragment); } return; diff --git a/lib/src/visual/components/animate_icon.comp.dart b/lib/src/visual/components/animate_icon.comp.dart index 5f721baf..afe42cb6 100644 --- a/lib/src/visual/components/animate_icon.comp.dart +++ b/lib/src/visual/components/animate_icon.comp.dart @@ -245,7 +245,7 @@ class EmojiAnimationComp extends StatelessWidget { return Lottie.asset( 'assets/animated_icons/${animatedIcons[emoji]}', repeat: repeat, - renderCache: RenderCache.raster, + renderCache: RenderCache.drawingCommands, ); } else if (isOneEmoji(emoji)) { return Text( diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart index f8b88a1b..d1bb2d6c 100644 --- a/lib/src/visual/views/onboarding/recover_passwordless.view.dart +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -60,26 +60,21 @@ class _RecoverPasswordlessState extends State { @override void initState() { super.initState(); - _secondFactorController.text = widget.initialEmailToken ?? ''; + final token = + widget.initialEmailToken ?? PasswordlessRecoveryService.lastEmailToken; + _secondFactorController.text = token ?? ''; + _emailTokenSubscription = PasswordlessRecoveryService .onEmailTokenReceived .stream .listen((token) async { if (mounted) { - final state = _onboardingState; - if (state != null && !state.emailRecoveryRequested) { - state.emailRecoveryRequested = true; - await KeyValueStore.update( - key: KeyValueKeys.onboardingState, - update: (s) => s.emailRecoveryRequested = true, - ); - } setState(() { _secondFactorController.text = token; }); } }); - _initAsync(); + _initAsync(token); } @override @@ -90,14 +85,14 @@ class _RecoverPasswordlessState extends State { super.dispose(); } - Future _initAsync() async { + Future _initAsync(String? initialToken) async { try { // 1. Load OnboardingState final state = await KeyValueStore.getModel( KeyValueKeys.onboardingState, ); - if (widget.initialEmailToken != null && !state.emailRecoveryRequested) { + if (initialToken != null && !state.emailRecoveryRequested) { state.emailRecoveryRequested = true; await KeyValueStore.update( key: KeyValueKeys.onboardingState, diff --git a/lib/src/visual/views/settings/chat/chat_reactions.view.dart b/lib/src/visual/views/settings/chat/chat_reactions.view.dart index d050baa4..c6dde23e 100644 --- a/lib/src/visual/views/settings/chat/chat_reactions.view.dart +++ b/lib/src/visual/views/settings/chat/chat_reactions.view.dart @@ -75,10 +75,16 @@ class _ChatReactionSelectionView extends State { ? context.color.primary.withAlpha(150) : context.color.surface, child: Center( - child: Text( - emoji, - style: const TextStyle(fontSize: 32), - ), + child: isSelected + ? SizedBox( + width: 40, + height: 40, + child: EmojiAnimationComp(emoji: emoji), + ) + : Text( + emoji, + style: const TextStyle(fontSize: 32), + ), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index e506b9cd..7b57597c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1266,7 +1266,7 @@ packages: path: "dependencies/lottie" relative: true source: path - version: "3.3.2" + version: "3.5.1" markdown: dependency: transitive description: From c64816b5f6bae68e0704b079eb201b87990bd4f8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 15 Jul 2026 22:16:42 +0200 Subject: [PATCH 07/29] bump version --- ios/Podfile.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f2578927..5532e351 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -215,4 +215,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf -COCOAPODS: 1.16.2 +COCOAPODS: 1.17.0 diff --git a/pubspec.yaml b/pubspec.yaml index 8a1dfc45..b69372a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.4.0+155 +version: 0.4.0+156 environment: sdk: ^3.11.0 From 1b372685774d3232e79bb792a58502650c67bf41 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 16 Jul 2026 10:31:41 +0200 Subject: [PATCH 08/29] clear unused strings --- .../generated/app_localizations.dart | 116 +----------------- .../generated/app_localizations_de.dart | 73 +---------- .../generated/app_localizations_en.dart | 72 +---------- lib/src/localization/translations | 2 +- .../import_from_gallery.view.dart | 10 +- 5 files changed, 12 insertions(+), 261 deletions(-) diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 10999072..c384fff3 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -926,12 +926,6 @@ abstract class AppLocalizations { /// **'Verified via link.'** String get verificationTypeLink; - /// No description provided for @verificationTypeContactSharedByVerified. - /// - /// In en, this message translates to: - /// **'Contact received from a verified contact.'** - String get verificationTypeContactSharedByVerified; - /// No description provided for @verificationTypeMigratedFromOldVersion. /// /// In en, this message translates to: @@ -1670,12 +1664,6 @@ abstract class AppLocalizations { /// **'Retransmission requested'** String get retransmissionRequested; - /// No description provided for @openChangeLog. - /// - /// In en, this message translates to: - /// **'Open changelog automatically'** - String get openChangeLog; - /// No description provided for @reportUserTitle. /// /// In en, this message translates to: @@ -2084,12 +2072,6 @@ abstract class AppLocalizations { /// **'wants to connect with you.'** String get notificationContactRequest; - /// No description provided for @notificationContactRequestUnknownUser. - /// - /// In en, this message translates to: - /// **'have received a new contact request.'** - String get notificationContactRequestUnknownUser; - /// No description provided for @notificationAcceptRequest. /// /// In en, this message translates to: @@ -2144,18 +2126,6 @@ abstract class AppLocalizations { /// **'has responded{inGroup}.'** String notificationResponse(Object inGroup); - /// No description provided for @notificationTitleUnknown. - /// - /// In en, this message translates to: - /// **'You have a new message.'** - String get notificationTitleUnknown; - - /// No description provided for @notificationBodyUnknown. - /// - /// In en, this message translates to: - /// **'Open twonly to learn more.'** - String get notificationBodyUnknown; - /// No description provided for @notificationCategoryMessageTitle. /// /// In en, this message translates to: @@ -2438,12 +2408,6 @@ abstract class AppLocalizations { /// **'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'** String get onboardingBackupBody; - /// No description provided for @onboardingVerificationBadgeTitle. - /// - /// In en, this message translates to: - /// **'Verification Badge'** - String get onboardingVerificationBadgeTitle; - /// No description provided for @onboardingUserDiscoveryShareFriends. /// /// In en, this message translates to: @@ -3434,18 +3398,6 @@ abstract class AppLocalizations { /// **'Step-by-step setup so you can decide for yourself.'** String get onboardingProfileSelectionCustomizeDesc; - /// No description provided for @onboardingProfileSelectionStrictTitle. - /// - /// In en, this message translates to: - /// **'Enhanced Protection'** - String get onboardingProfileSelectionStrictTitle; - - /// No description provided for @onboardingProfileSelectionStrictDesc. - /// - /// In en, this message translates to: - /// **'Maximum anti-phishing defense. Recommended for *journalists & public figures*.'** - String get onboardingProfileSelectionStrictDesc; - /// No description provided for @replyFlameRestored. /// /// In en, this message translates to: @@ -3458,24 +3410,6 @@ abstract class AppLocalizations { /// **'Ask a friend'** String get replyAskAFriend; - /// No description provided for @unverifiedWarningDirectTitle. - /// - /// In en, this message translates to: - /// **'Identity not verified in person'** - String get unverifiedWarningDirectTitle; - - /// No description provided for @unverifiedWarningGroupTitle. - /// - /// In en, this message translates to: - /// **'Not all members are verified in person'** - String get unverifiedWarningGroupTitle; - - /// No description provided for @unverifiedWarningBody. - /// - /// In en, this message translates to: - /// **'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'** - String get unverifiedWarningBody; - /// No description provided for @unverifiedWarningButton. /// /// In en, this message translates to: @@ -3563,12 +3497,8 @@ abstract class AppLocalizations { /// No description provided for @importGalleryComplete. /// /// In en, this message translates to: - /// **'Import complete: {imported} successfully imported, {duplicated} duplicated and {failed} failed.'** - String importGalleryComplete( - Object imported, - Object duplicated, - Object failed, - ); + /// **'Import complete: {imported} successfully imported and {failed} failed.'** + String importGalleryComplete(Object imported, Object failed); /// No description provided for @importGalleryGrantAccess. /// @@ -3804,54 +3734,18 @@ abstract class AppLocalizations { /// **'These friends either have not yet received their share or do not use twonly actively anymore, which could mean that they maybe cannot help.'** String get passwordlessRecoveryInactiveFriendsDesc; - /// No description provided for @passwordlessRecoveryNotEnoughFriends. - /// - /// In en, this message translates to: - /// **'Not enough friends selected. You need at least 3.'** - String get passwordlessRecoveryNotEnoughFriends; - - /// No description provided for @passwordlessRecoveryLoading. - /// - /// In en, this message translates to: - /// **'Loading...'** - String get passwordlessRecoveryLoading; - /// No description provided for @passwordlessRecoveryEnableSuccess. /// /// In en, this message translates to: /// **'Passwordless recovery successfully enabled!'** String get passwordlessRecoveryEnableSuccess; - /// No description provided for @passwordlessRecoveryEnableFailed. - /// - /// In en, this message translates to: - /// **'Failed to enable passwordless recovery.'** - String get passwordlessRecoveryEnableFailed; - - /// No description provided for @passwordlessRecoveryNeedAtLeast3. - /// - /// In en, this message translates to: - /// **'You need at least 3 trusted friends.'** - String get passwordlessRecoveryNeedAtLeast3; - - /// No description provided for @passwordlessRecoveryInvalidPin. - /// - /// In en, this message translates to: - /// **'Invalid PIN'** - String get passwordlessRecoveryInvalidPin; - /// No description provided for @passwordlessRecoveryEnterPin. /// /// In en, this message translates to: /// **'Please enter a PIN.'** String get passwordlessRecoveryEnterPin; - /// No description provided for @passwordlessRecoveryPinMinLength. - /// - /// In en, this message translates to: - /// **'PIN must be at least 4 digits.'** - String get passwordlessRecoveryPinMinLength; - /// No description provided for @passwordlessRecoveryEnterEmail. /// /// In en, this message translates to: @@ -4134,12 +4028,6 @@ abstract class AppLocalizations { /// **'Yes ({seconds}s)'** String passwordlessRecoveryYesWithTimer(Object seconds); - /// No description provided for @recoverPasswordlessBeingRecoveredLabel. - /// - /// In en, this message translates to: - /// **'Account being recovered'** - String get recoverPasswordlessBeingRecoveredLabel; - /// No description provided for @recoverPasswordlessSharesReceived. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index f0daeed9..062d9b29 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -459,10 +459,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get verificationTypeLink => 'Per Link verifiziert.'; - @override - String get verificationTypeContactSharedByVerified => - 'Von einem verifizierten Kontakt geteilt bekommen.'; - @override String get verificationTypeMigratedFromOldVersion => 'Von alter Version migriert'; @@ -883,9 +879,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get retransmissionRequested => 'Wird erneut versucht.'; - @override - String get openChangeLog => 'Changelog automatisch öffnen'; - @override String reportUserTitle(Object username) { return 'Melde $username'; @@ -1146,10 +1139,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get notificationContactRequest => 'möchte sich mit dir vernetzen.'; - @override - String get notificationContactRequestUnknownUser => - 'hast eine neue Kontaktanfrage erhalten.'; - @override String get notificationAcceptRequest => 'ist jetzt mit dir vernetzt.'; @@ -1187,12 +1176,6 @@ class AppLocalizationsDe extends AppLocalizations { return 'hat dir$inGroup geantwortet.'; } - @override - String get notificationTitleUnknown => 'Du hast eine neue Nachricht.'; - - @override - String get notificationBodyUnknown => 'Öffne twonly um mehr zu erfahren.'; - @override String get notificationCategoryMessageTitle => 'Nachrichten'; @@ -1353,9 +1336,6 @@ class AppLocalizationsDe extends AppLocalizations { String get onboardingBackupBody => 'Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.'; - @override - String get onboardingVerificationBadgeTitle => 'Verifizierungs-Haken'; - @override String get onboardingUserDiscoveryShareFriends => 'Freunde teilen'; @@ -1972,31 +1952,12 @@ class AppLocalizationsDe extends AppLocalizations { String get onboardingProfileSelectionCustomizeDesc => 'Schritt-für-Schritt-Einrichtung, damit du selbst entscheiden kannst.'; - @override - String get onboardingProfileSelectionStrictTitle => 'Erhöhter Schutz'; - - @override - String get onboardingProfileSelectionStrictDesc => - 'Maximaler Schutz vor Phishing. Empfohlen für *Journalisten & Personen des öffentlichen Lebens*.'; - @override String get replyFlameRestored => 'Flammen wiederhergestellt'; @override String get replyAskAFriend => 'Einen Freund fragen'; - @override - String get unverifiedWarningDirectTitle => - 'Identität nicht persönlich verifiziert'; - - @override - String get unverifiedWarningGroupTitle => - 'Nicht alle Mitglieder sind persönlich verifiziert'; - - @override - String get unverifiedWarningBody => - '*Teile keine geheimen Daten*. Jemand könnte sich *als dein Freund ausgeben*.'; - @override String get unverifiedWarningButton => 'Jetzt verifizieren'; @@ -2048,12 +2009,8 @@ class AppLocalizationsDe extends AppLocalizations { String get importGalleryStarting => 'Import wird gestartet...'; @override - String importGalleryComplete( - Object imported, - Object duplicated, - Object failed, - ) { - return 'Import abgeschlossen: $imported erfolgreich importiert, $duplicated Duplikate und $failed fehlgeschlagen.'; + String importGalleryComplete(Object imported, Object failed) { + return 'Import abgeschlossen: $imported erfolgreich importiert und $failed fehlgeschlagen.'; } @override @@ -2190,35 +2147,13 @@ class AppLocalizationsDe extends AppLocalizations { String get passwordlessRecoveryInactiveFriendsDesc => 'Diese Freunde haben entweder ihren Teil noch nicht erhalten oder nutzen twonly nicht mehr aktiv, was bedeutet, dass sie möglicherweise nicht helfen können.'; - @override - String get passwordlessRecoveryNotEnoughFriends => - 'Nicht genügend Freunde ausgewählt. Du brauchst mindestens 3.'; - - @override - String get passwordlessRecoveryLoading => 'Wird geladen...'; - @override String get passwordlessRecoveryEnableSuccess => 'Passwortloses Backup erfolgreich aktiviert!'; - @override - String get passwordlessRecoveryEnableFailed => - 'Fehler beim Aktivieren des passwortlosen Backups.'; - - @override - String get passwordlessRecoveryNeedAtLeast3 => - 'Du brauchst mindestens 3 Freunde.'; - - @override - String get passwordlessRecoveryInvalidPin => 'Ungültige PIN'; - @override String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.'; - @override - String get passwordlessRecoveryPinMinLength => - 'Die PIN muss mindestens 4 Ziffern lang sein.'; - @override String get passwordlessRecoveryEnterEmail => 'Bitte gib eine E-Mail-Adresse ein.'; @@ -2396,10 +2331,6 @@ class AppLocalizationsDe extends AppLocalizations { return 'Ja (${seconds}s)'; } - @override - String get recoverPasswordlessBeingRecoveredLabel => - 'Konto wird wiederhergestellt'; - @override String recoverPasswordlessSharesReceived(Object received, Object threshold) { return '$received von $threshold Freunden haben geholfen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index f0f3dc5e..c84e928f 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -455,10 +455,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get verificationTypeLink => 'Verified via link.'; - @override - String get verificationTypeContactSharedByVerified => - 'Contact received from a verified contact.'; - @override String get verificationTypeMigratedFromOldVersion => 'Migrated from old version.'; @@ -878,9 +874,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get retransmissionRequested => 'Retransmission requested'; - @override - String get openChangeLog => 'Open changelog automatically'; - @override String reportUserTitle(Object username) { return 'Report $username'; @@ -1140,10 +1133,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get notificationContactRequest => 'wants to connect with you.'; - @override - String get notificationContactRequestUnknownUser => - 'have received a new contact request.'; - @override String get notificationAcceptRequest => 'is now connected with you.'; @@ -1181,12 +1170,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'has responded$inGroup.'; } - @override - String get notificationTitleUnknown => 'You have a new message.'; - - @override - String get notificationBodyUnknown => 'Open twonly to learn more.'; - @override String get notificationCategoryMessageTitle => 'Messages'; @@ -1345,9 +1328,6 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingBackupBody => 'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'; - @override - String get onboardingVerificationBadgeTitle => 'Verification Badge'; - @override String get onboardingUserDiscoveryShareFriends => 'Share your friends'; @@ -1958,30 +1938,12 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingProfileSelectionCustomizeDesc => 'Step-by-step setup so you can decide for yourself.'; - @override - String get onboardingProfileSelectionStrictTitle => 'Enhanced Protection'; - - @override - String get onboardingProfileSelectionStrictDesc => - 'Maximum anti-phishing defense. Recommended for *journalists & public figures*.'; - @override String get replyFlameRestored => 'Flames restored'; @override String get replyAskAFriend => 'Ask a friend'; - @override - String get unverifiedWarningDirectTitle => 'Identity not verified in person'; - - @override - String get unverifiedWarningGroupTitle => - 'Not all members are verified in person'; - - @override - String get unverifiedWarningBody => - '*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'; - @override String get unverifiedWarningButton => 'Verify now'; @@ -2033,12 +1995,8 @@ class AppLocalizationsEn extends AppLocalizations { String get importGalleryStarting => 'Starting import...'; @override - String importGalleryComplete( - Object imported, - Object duplicated, - Object failed, - ) { - return 'Import complete: $imported successfully imported, $duplicated duplicated and $failed failed.'; + String importGalleryComplete(Object imported, Object failed) { + return 'Import complete: $imported successfully imported and $failed failed.'; } @override @@ -2175,35 +2133,13 @@ class AppLocalizationsEn extends AppLocalizations { String get passwordlessRecoveryInactiveFriendsDesc => 'These friends either have not yet received their share or do not use twonly actively anymore, which could mean that they maybe cannot help.'; - @override - String get passwordlessRecoveryNotEnoughFriends => - 'Not enough friends selected. You need at least 3.'; - - @override - String get passwordlessRecoveryLoading => 'Loading...'; - @override String get passwordlessRecoveryEnableSuccess => 'Passwordless recovery successfully enabled!'; - @override - String get passwordlessRecoveryEnableFailed => - 'Failed to enable passwordless recovery.'; - - @override - String get passwordlessRecoveryNeedAtLeast3 => - 'You need at least 3 trusted friends.'; - - @override - String get passwordlessRecoveryInvalidPin => 'Invalid PIN'; - @override String get passwordlessRecoveryEnterPin => 'Please enter a PIN.'; - @override - String get passwordlessRecoveryPinMinLength => - 'PIN must be at least 4 digits.'; - @override String get passwordlessRecoveryEnterEmail => 'Please enter an email address.'; @@ -2374,10 +2310,6 @@ class AppLocalizationsEn extends AppLocalizations { return 'Yes (${seconds}s)'; } - @override - String get recoverPasswordlessBeingRecoveredLabel => - 'Account being recovered'; - @override String recoverPasswordlessSharesReceived(Object received, Object threshold) { return '$received out of $threshold friends have helped'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 2a54bd8e..ef0aed22 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 2a54bd8e7f2021c5159c92b2e1dff8c8ed4effdc +Subproject commit ef0aed22f39a7453a9dbdbe26a098159b792dbac diff --git a/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart b/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart index 6755eace..4a257d32 100644 --- a/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart @@ -64,7 +64,6 @@ class _ImportFromGalleryViewState extends State { final total = uris.length; var importedCount = 0; - var duplicated = 0; var failedCount = 0; for (final uri in uris) { @@ -137,7 +136,6 @@ class _ImportFromGalleryViewState extends State { context, context.lang.importGalleryComplete( importedCount, - duplicated, failedCount, ), level: SnackbarLevel.success, @@ -329,7 +327,6 @@ class _ImportFromGalleryViewState extends State { .toList(); final total = selectedAssets.length; var importedCount = 0; - var duplicated = 0; var failedCount = 0; for (final asset in selectedAssets) { @@ -352,7 +349,11 @@ class _ImportFromGalleryViewState extends State { final exists = await twonlyDB.mediaFilesDao.getMediaByHash(hash); if (exists.isNotEmpty) { - duplicated += 1; + final mediaFile = exists.first; + final mediaService = MediaFileService(mediaFile); + await mediaService.storedPath.parent.create(recursive: true); + await file.copy(mediaService.storedPath.path); + importedCount++; continue; } @@ -446,7 +447,6 @@ class _ImportFromGalleryViewState extends State { context, context.lang.importGalleryComplete( importedCount, - duplicated, failedCount, ), level: SnackbarLevel.success, From df9b0d66bd937661340937f3e03d49bcfd14b161 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 16 Jul 2026 10:31:58 +0200 Subject: [PATCH 09/29] fix comments and show shared contacts avatar --- .../model/json/onboarding_state.model.dart | 3 +- .../model/json/onboarding_state.model.g.dart | 4 +++ lib/src/services/backup.service.dart | 2 +- .../passwordless_recovery.service.dart | 36 +++++++++---------- .../visual/elements/contact_chip.element.dart | 8 +++++ .../visual/views/chats/media_viewer.view.dart | 2 +- .../onboarding/recover_passwordless.view.dart | 1 + 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/lib/src/model/json/onboarding_state.model.dart b/lib/src/model/json/onboarding_state.model.dart index cdd4eac4..a9fc7909 100644 --- a/lib/src/model/json/onboarding_state.model.dart +++ b/lib/src/model/json/onboarding_state.model.dart @@ -14,6 +14,7 @@ class ReceivedRecoveryShare { required this.myAvatarSvg, required this.threshold, required this.sharedSecretDataBytes, + this.trustedFriendAvatarSvg, }); factory ReceivedRecoveryShare.fromJson(Map json) => @@ -22,6 +23,7 @@ class ReceivedRecoveryShare { final int messageId; final String trustedFriendDisplayName; + final List? trustedFriendAvatarSvg; final String myDisplayName; final int myUserId; @@ -58,7 +60,6 @@ class OnboardingState { List? encryptionKey; bool emailRecoveryRequested; - List receivedShares = []; Map toJson() => _$OnboardingStateToJson(this); diff --git a/lib/src/model/json/onboarding_state.model.g.dart b/lib/src/model/json/onboarding_state.model.g.dart index 67ba5930..7987797a 100644 --- a/lib/src/model/json/onboarding_state.model.g.dart +++ b/lib/src/model/json/onboarding_state.model.g.dart @@ -11,6 +11,9 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson( ) => ReceivedRecoveryShare( messageId: (json['messageId'] as num).toInt(), trustedFriendDisplayName: json['trustedFriendDisplayName'] as String, + trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), myDisplayName: json['myDisplayName'] as String, myUserId: (json['myUserId'] as num).toInt(), myAvatarSvg: (json['myAvatarSvg'] as List?) @@ -27,6 +30,7 @@ Map _$ReceivedRecoveryShareToJson( ) => { 'messageId': instance.messageId, 'trustedFriendDisplayName': instance.trustedFriendDisplayName, + 'trustedFriendAvatarSvg': instance.trustedFriendAvatarSvg, 'myDisplayName': instance.myDisplayName, 'myUserId': instance.myUserId, 'myAvatarSvg': instance.myAvatarSvg, diff --git a/lib/src/services/backup.service.dart b/lib/src/services/backup.service.dart index 42fb4e67..242166b2 100644 --- a/lib/src/services/backup.service.dart +++ b/lib/src/services/backup.service.dart @@ -247,7 +247,7 @@ class BackupService { } if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) { - // The KeyManager was restored sucessfully, restore the archive now. + // The KeyManager was restored successfully, restore the archive now. try { final downloadToken = await RustBackupArchive.getBackupDownloadToken(); diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index ced9abd4..ecca9060 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -187,36 +187,29 @@ class PasswordlessRecoveryService { switch (secondFactorType) { case SecondFactorType.email: - - // The serverKey is encrypted with the email; this protects the server from seeing the user's email address, while - // also ensuring that the server can only send the real secret to the user's configured email, as a different - // email will result in a different secret key. - // This is only stored so the user can see there email, and verify that he has set the valid mail... config.email = secondFactorValue; emailHint = createEmailHint(secondFactorValue); // E-Mail Protection: - // - Server can only learn the email during recovery. Ensured as the server gets the NONCE and MAC to decrpyt during recovery. - // - Trusted-friends: Server key is send to the mail, they whould need access to the user's mail account. + // - Server can only learn the email during recovery. Ensured as the server gets the NONCE to decrypt only during recovery. + // - Trusted-friends: Server key is only sent to the mail, they would need access to the user's mail account. secondFactorEncryptedServerKeyKey = SecretKey( Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes), ); case SecondFactorType.pin: - // The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin. + // The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin config.pinSeed = getRandomUint8List(32); - // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after X tries, the - // unlock token is required to prevent a malicous user (except the trusted friends) to triger this deletion. + // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the + // unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion. config.pinUnlockToken = getRandomUint8List(32); // Brute-force protection for the user's pin: // - Server: Does not know the seed. - // - Trusted friends: - // Can only check the result X times before the server deletes the key. As they do not have - // the mac and the cypher text they are unable to brute-force the pin localy. And the server only allows 10 - // tries. + // - Trusted friends: Can only check the result 10 times before the server deletes the key. As they do not have + // the mac and the cipher text they are unable to brute-force the pin locally. And the server only allows 10 tries. final pinProtectionKey = await Hmac.sha256().calculateMac( Uint8List.fromList(utf8.encode(secondFactorValue)), secretKey: SecretKey(config.pinSeed!), @@ -224,7 +217,7 @@ class PasswordlessRecoveryService { // To restore the user has to provide the server with this encryption key. The server then can verify the // correct pin was entered, when decrypting the server key as he also receives the mac and nonce from the user - // during recovery. Only when the mac is correct the server provides the user with the serverKey. + // Only when the mac is correct the server provides the user with the serverKey. secondFactorEncryptedServerKeyKey = SecretKey(pinProtectionKey.bytes); case SecondFactorType.none: @@ -232,7 +225,7 @@ class PasswordlessRecoveryService { if (secondFactorEncryptedServerKeyKey != null) { // The server key is used to encrypt the RecoveryData of the users. This ensures that when the trusted friends - // colaberate, they additional need the serverKey to decrypt the user's key. + // collaborate, they additionally need the serverKey to decrypt the user's key. serverKey = getRandomUint8List(32); final secretBox = await xchacha20.encrypt( @@ -243,7 +236,7 @@ class PasswordlessRecoveryService { // The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit // because of XChaCha), he is unable to decrypt the server key without the help of the trusted friends. This - // ensures that the server never learns the users orginal pin, as he is missing the pin_seed and also unable to + // ensures that the server never learns the user's original pin, as he is missing the pin_seed and also unable to // brute-force the email of the user as he does not have the nonce. encryptedServerKey = Uint8List.fromList([ ...secretBox.cipherText, @@ -323,7 +316,7 @@ class PasswordlessRecoveryService { unawaited(performHeartbeat()); - // The passwordless is configured sucessfully. + // The passwordless is configured successfully. return true; } @@ -396,7 +389,7 @@ class PasswordlessRecoveryService { clock.now().difference(lastContactHeartbeat).inHours >= 24; if (isContactHeartbeatOlderThan24h) { - // Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send. + // Get all contacts where recoveryLastHeartbeat is NULL. Then for each contact send. // recoveryLastHeartbeat is ONLY updated in case the contact has responded. final pendingShares = await (twonlyDB.select(twonlyDB.contacts)..where( @@ -542,7 +535,7 @@ class PasswordlessRecoveryService { final recoveryLastHeartbeat = const ListEquality().equals(computedHash, msg.hash) ? clock.now() - : null; // The stored share not valid (maybe a old backup was restored). This will cause the performHeartbeat to resend him his share + : null; // The stored share not valid (maybe an old backup was restored). This will cause the performHeartbeat to resend him his share Log.info( '[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)', ); @@ -602,6 +595,9 @@ class PasswordlessRecoveryService { final receivedShare = ReceivedRecoveryShare( messageId: msgId, trustedFriendDisplayName: share.trustedFriend.displayName, + trustedFriendAvatarSvg: share.trustedFriend.hasAvatar() + ? share.trustedFriend.avatar + : null, myDisplayName: share.shareUser.displayName, myUserId: share.shareUser.userId.toInt(), myAvatarSvg: share.shareUser.hasAvatar() diff --git a/lib/src/visual/elements/contact_chip.element.dart b/lib/src/visual/elements/contact_chip.element.dart index f74356c1..e4974c02 100644 --- a/lib/src/visual/elements/contact_chip.element.dart +++ b/lib/src/visual/elements/contact_chip.element.dart @@ -1,7 +1,10 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; @@ -12,6 +15,7 @@ class ContactChip extends StatelessWidget { const ContactChip({ this.contact, this.username, + this.avatarSvg, this.onTap, super.key, }) : assert( @@ -21,6 +25,7 @@ class ContactChip extends StatelessWidget { final Contact? contact; final String? username; + final List? avatarSvg; final void Function(int)? onTap; @override @@ -43,6 +48,9 @@ class ContactChip extends StatelessWidget { avatar: AvatarIcon( contactId: contact?.userId, fontSize: 10, + svg: avatarSvg != null + ? getAvatarSvg(Uint8List.fromList(avatarSvg!)) + : null, ), label: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 50916694..5cc36dda 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -364,7 +364,7 @@ class _MediaViewerViewState extends State { return advanceToNextMediaOrExit(); } - // The server can now delete the encrypted bytes, as the users has sucessfully opened it. + // The server can now delete the encrypted bytes, as the user has successfully opened it. Log.info( 'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}', ); diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart index d1bb2d6c..045d54b6 100644 --- a/lib/src/visual/views/onboarding/recover_passwordless.view.dart +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -543,6 +543,7 @@ class _RecoverPasswordlessState extends State { children: shares.map((share) { return ContactChip( username: share.trustedFriendDisplayName, + avatarSvg: share.trustedFriendAvatarSvg, ); }).toList(), ), From bb8f33e9d6f7279ed42ba4093d91ed8e09fdae01 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 18 Jul 2026 20:10:58 +0200 Subject: [PATCH 10/29] fix badge color and show users avatar --- lib/src/visual/components/verification_badge.comp.dart | 4 +++- .../user_discovery/components/user_discovery_setup.comp.dart | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/visual/components/verification_badge.comp.dart b/lib/src/visual/components/verification_badge.comp.dart index 0d096caa..c7be91d3 100644 --- a/lib/src/visual/components/verification_badge.comp.dart +++ b/lib/src/visual/components/verification_badge.comp.dart @@ -33,7 +33,9 @@ class VerificationBadgeComp extends StatefulWidget { } class _VerificationBadgeCompState extends State { - late bool _isVerified = widget.contact?.verified ?? true; + late bool _isVerified = + widget.contact?.verified ?? + !(widget.isVerifiedByTransferredTrust ?? false); bool _isSharedVerified = false; int _verifiedByTransferredTrustCount = 0; int _sharedByVerifiedCount = 0; diff --git a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart index 57e66462..c04b5fc4 100644 --- a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart +++ b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart @@ -436,7 +436,10 @@ class UserDiscoverySetupComp extends StatelessWidget { ), child: Row( children: [ - const AvatarIcon(fontSize: 14), + const AvatarIcon( + fontSize: 14, + myAvatar: true, + ), const SizedBox(width: 8), Expanded( child: Column( From 6284345b57ae9281b62dbad598fadf241268bab7 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 18 Jul 2026 20:25:28 +0200 Subject: [PATCH 11/29] pop until contact view after sucessfull verification --- .../services/key_verification.service.dart | 19 +++ .../components/profile_qr_code.comp.dart | 116 ++++++++++-------- .../verification_success_dialog.comp.dart | 15 ++- .../views/camera/camera_qr_scanner.view.dart | 7 +- 4 files changed, 99 insertions(+), 58 deletions(-) diff --git a/lib/src/services/key_verification.service.dart b/lib/src/services/key_verification.service.dart index 62ee6a21..df656698 100644 --- a/lib/src/services/key_verification.service.dart +++ b/lib/src/services/key_verification.service.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; @@ -17,6 +18,24 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart'; class KeyVerificationService { + static final StreamController _verificationSuccessCloseController = + StreamController.broadcast(); + + static Stream get onVerificationSuccessClose => + _verificationSuccessCloseController.stream; + + static void notifyVerificationSuccessClose() { + _verificationSuccessCloseController.add(null); + } + + static void closeVerificationFlows(BuildContext context) { + Navigator.popUntil(context, (route) { + final name = route.settings.name; + if (name == null) return false; + return !name.contains('qr_scanner') && !name.contains('verifybadge'); + }); + } + static Future> getNewSecretVerificationToken() async { final token = getRandomUint8List(16); await twonlyDB.keyVerificationDao.insertVerificationToken(token); diff --git a/lib/src/visual/components/profile_qr_code.comp.dart b/lib/src/visual/components/profile_qr_code.comp.dart index c10eb852..3d113a07 100644 --- a/lib/src/visual/components/profile_qr_code.comp.dart +++ b/lib/src/visual/components/profile_qr_code.comp.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; @@ -22,59 +24,73 @@ class ProfileQrCodeComp extends StatefulWidget { BuildContext context, { Contact? contact, bool openToVerify = false, - }) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return Container( - decoration: BoxDecoration( - color: context.color.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(24), + }) async { + StreamSubscription? subscription; + + if (openToVerify) { + subscription = KeyVerificationService.onVerificationSuccessClose.listen((_) { + if (context.mounted) { + KeyVerificationService.closeVerificationFlows(context); + } + }); + } + + try { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return Container( + decoration: BoxDecoration( + color: context.color.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), ), - ), - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: double.infinity), - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.color.onSurface.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(2), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: double.infinity), + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: context.color.onSurface.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(2), + ), ), - ), - const SizedBox(height: 24), - Text( - openToVerify - ? (contact != null - ? context.lang.letUserScanQrCode( - getContactDisplayName(contact), - ) - : context.lang.letFriendScanQrToVerify) - : (contact != null - ? context.lang.letUserScanQrCode( - getContactDisplayName(contact), - ) - : context.lang.addContactQrSheetSubtext), - style: TextStyle( - fontSize: 14, - color: context.color.onSurface.withValues(alpha: 0.6), + const SizedBox(height: 24), + Text( + openToVerify + ? (contact != null + ? context.lang.letUserScanQrCode( + getContactDisplayName(contact), + ) + : context.lang.letFriendScanQrToVerify) + : (contact != null + ? context.lang.letUserScanQrCode( + getContactDisplayName(contact), + ) + : context.lang.addContactQrSheetSubtext), + style: TextStyle( + fontSize: 14, + color: context.color.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - const ProfileQrCodeComp(), - const SizedBox(height: 34), - ], - ), - ); - }, - ); + const SizedBox(height: 24), + const ProfileQrCodeComp(), + const SizedBox(height: 34), + ], + ), + ); + }, + ); + } finally { + await subscription?.cancel(); + } } @override diff --git a/lib/src/visual/components/verification_success_dialog.comp.dart b/lib/src/visual/components/verification_success_dialog.comp.dart index ee48d804..0c3be280 100644 --- a/lib/src/visual/components/verification_success_dialog.comp.dart +++ b/lib/src/visual/components/verification_success_dialog.comp.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_success_animation.comp.dart'; @@ -18,11 +19,16 @@ class VerificationSuccessDialog extends StatelessWidget { final String? message; /// Utility method to easily present this dialog. - static Future show(BuildContext context, Contact contact, {String? message}) { + static Future show( + BuildContext context, + Contact contact, { + String? message, + }) { return showDialog( context: context, barrierDismissible: false, - builder: (context) => VerificationSuccessDialog(contact: contact, message: message), + builder: (context) => + VerificationSuccessDialog(contact: contact, message: message), ); } @@ -75,7 +81,10 @@ class VerificationSuccessDialog extends StatelessWidget { ), const SizedBox(height: 28), MyButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + KeyVerificationService.notifyVerificationSuccessClose(); + Navigator.pop(context); + }, child: Text(context.lang.close), ), ], diff --git a/lib/src/visual/views/camera/camera_qr_scanner.view.dart b/lib/src/visual/views/camera/camera_qr_scanner.view.dart index d23b1b7d..c5f45f9f 100644 --- a/lib/src/visual/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/visual/views/camera/camera_qr_scanner.view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; @@ -34,11 +35,7 @@ class QrCodeScannerViewState extends State { if (widget.openToVerify && widget.contact != null) { _mainCameraController.onVerificationSuccessDismissed = (contact) { if (mounted) { - Navigator.popUntil(context, (route) { - final name = route.settings.name; - if (name == null) return true; - return !name.contains('qr_scanner') && !name.contains('verifybadge'); - }); + KeyVerificationService.closeVerificationFlows(context); } }; } From e4e1cf592df85103980855b871576adc3c5220fd Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 13:15:41 +0200 Subject: [PATCH 12/29] update protobuf --- .../push_notification.pb.swift | 26 +- .../api/websocket/client_to_server.pb.dart | 421 +++++++++++++++- .../websocket/client_to_server.pbjson.dart | 155 +++++- .../api/websocket/server_to_client.pb.dart | 464 +++++++++++++++++- .../websocket/server_to_client.pbjson.dart | 254 +++++++--- 5 files changed, 1217 insertions(+), 103 deletions(-) diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index 1f8a210d..09b28f19 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -20,12 +20,12 @@ import SwiftProtobuf // incompatible with the version of SwiftProtobuf to which you are linking. // Please ensure that you are building against the same version of the API // that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { +fileprivate nonisolated struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} typealias Version = _2 } -enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { +nonisolated enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case reaction // = 0 case response // = 1 @@ -119,7 +119,7 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { } -struct EncryptedPushNotification: Sendable { +nonisolated struct EncryptedPushNotification: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -137,7 +137,7 @@ struct EncryptedPushNotification: Sendable { init() {} } -struct PushNotification: Sendable { +nonisolated struct PushNotification: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -170,7 +170,7 @@ struct PushNotification: Sendable { fileprivate var _additionalContent: String? = nil } -struct PushUsers: Sendable { +nonisolated struct PushUsers: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -182,7 +182,7 @@ struct PushUsers: Sendable { init() {} } -struct PushUser: Sendable { +nonisolated struct PushUser: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -211,7 +211,7 @@ struct PushUser: Sendable { fileprivate var _lastMessageID: String? = nil } -struct PushKey: Sendable { +nonisolated struct PushKey: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -229,11 +229,11 @@ struct PushKey: Sendable { // MARK: - Code below here is support for the SwiftProtobuf runtime. -extension PushKind: SwiftProtobuf._ProtoNameProviding { +nonisolated extension PushKind: SwiftProtobuf._ProtoNameProviding { static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0REACTION\0\u{1}RESPONSE\0\u{1}TEXT\0\u{1}VIDEO\0\u{1}TWONLY\0\u{1}IMAGE\0\u{1}CONTACT_REQUEST\0\u{1}ACCEPT_REQUEST\0\u{1}STORED_MEDIA_FILE\0\u{1}TEST_NOTIFICATION\0\u{1}REOPENED_MEDIA\0\u{1}REACTION_TO_VIDEO\0\u{1}REACTION_TO_TEXT\0\u{1}REACTION_TO_IMAGE\0\u{1}REACTION_TO_AUDIO\0\u{1}ADDED_TO_GROUP\0\u{1}AUDIO\0") } -extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +nonisolated extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "EncryptedPushNotification" static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_id\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0") @@ -278,7 +278,7 @@ extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._Messa } } -extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +nonisolated extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushNotification" static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}message_id\0\u{3}additional_content\0") @@ -322,7 +322,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme } } -extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +nonisolated extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushUsers" static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}users\0") @@ -352,7 +352,7 @@ extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation } } -extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +nonisolated extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushUser" static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}display_name\0\u{1}blocked\0\u{3}last_message_id\0\u{3}push_keys\0") @@ -406,7 +406,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB } } -extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +nonisolated extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushKey" static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{3}created_at_unix_timestamp\0") diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index b592eea1..7ca11faa 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -2712,6 +2712,311 @@ class ApplicationData_PasswordlessNotification extends $pb.GeneratedMessage { void clearEncryptedMessage() => $_clearField(2); } +class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage { + factory ApplicationData_RequestMemoriesUpload({ + $fixnum.Int64? size, + $fixnum.Int64? originalDate, + }) { + final result = create(); + if (size != null) result.size = size; + if (originalDate != null) result.originalDate = originalDate; + return result; + } + + ApplicationData_RequestMemoriesUpload._(); + + factory ApplicationData_RequestMemoriesUpload.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_RequestMemoriesUpload.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.RequestMemoriesUpload', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'size') + ..aInt64(2, _omitFieldNames ? '' : 'originalDate') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_RequestMemoriesUpload clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_RequestMemoriesUpload copyWith( + void Function(ApplicationData_RequestMemoriesUpload) updates) => + super.copyWith((message) => + updates(message as ApplicationData_RequestMemoriesUpload)) + as ApplicationData_RequestMemoriesUpload; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_RequestMemoriesUpload create() => + ApplicationData_RequestMemoriesUpload._(); + @$core.override + ApplicationData_RequestMemoriesUpload createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_RequestMemoriesUpload getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ApplicationData_RequestMemoriesUpload>(create); + static ApplicationData_RequestMemoriesUpload? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get size => $_getI64(0); + @$pb.TagNumber(1) + set size($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasSize() => $_has(0); + @$pb.TagNumber(1) + void clearSize() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get originalDate => $_getI64(1); + @$pb.TagNumber(2) + set originalDate($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasOriginalDate() => $_has(1); + @$pb.TagNumber(2) + void clearOriginalDate() => $_clearField(2); +} + +class ApplicationData_ConfirmMemoriesUpload extends $pb.GeneratedMessage { + factory ApplicationData_ConfirmMemoriesUpload({ + $core.String? mediaId, + }) { + final result = create(); + if (mediaId != null) result.mediaId = mediaId; + return result; + } + + ApplicationData_ConfirmMemoriesUpload._(); + + factory ApplicationData_ConfirmMemoriesUpload.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_ConfirmMemoriesUpload.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.ConfirmMemoriesUpload', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mediaId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_ConfirmMemoriesUpload clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_ConfirmMemoriesUpload copyWith( + void Function(ApplicationData_ConfirmMemoriesUpload) updates) => + super.copyWith((message) => + updates(message as ApplicationData_ConfirmMemoriesUpload)) + as ApplicationData_ConfirmMemoriesUpload; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_ConfirmMemoriesUpload create() => + ApplicationData_ConfirmMemoriesUpload._(); + @$core.override + ApplicationData_ConfirmMemoriesUpload createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_ConfirmMemoriesUpload getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ApplicationData_ConfirmMemoriesUpload>(create); + static ApplicationData_ConfirmMemoriesUpload? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => $_clearField(1); +} + +class ApplicationData_GetMemoriesList extends $pb.GeneratedMessage { + factory ApplicationData_GetMemoriesList({ + $fixnum.Int64? offsetDate, + $fixnum.Int64? limit, + }) { + final result = create(); + if (offsetDate != null) result.offsetDate = offsetDate; + if (limit != null) result.limit = limit; + return result; + } + + ApplicationData_GetMemoriesList._(); + + factory ApplicationData_GetMemoriesList.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_GetMemoriesList.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.GetMemoriesList', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'offsetDate') + ..aInt64(2, _omitFieldNames ? '' : 'limit') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_GetMemoriesList clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_GetMemoriesList copyWith( + void Function(ApplicationData_GetMemoriesList) updates) => + super.copyWith( + (message) => updates(message as ApplicationData_GetMemoriesList)) + as ApplicationData_GetMemoriesList; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_GetMemoriesList create() => + ApplicationData_GetMemoriesList._(); + @$core.override + ApplicationData_GetMemoriesList createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_GetMemoriesList getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ApplicationData_GetMemoriesList? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get offsetDate => $_getI64(0); + @$pb.TagNumber(1) + set offsetDate($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasOffsetDate() => $_has(0); + @$pb.TagNumber(1) + void clearOffsetDate() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get limit => $_getI64(1); + @$pb.TagNumber(2) + set limit($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasLimit() => $_has(1); + @$pb.TagNumber(2) + void clearLimit() => $_clearField(2); +} + +class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage { + factory ApplicationData_GetMemoriesUrl({ + $core.String? mediaId, + }) { + final result = create(); + if (mediaId != null) result.mediaId = mediaId; + return result; + } + + ApplicationData_GetMemoriesUrl._(); + + factory ApplicationData_GetMemoriesUrl.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_GetMemoriesUrl.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.GetMemoriesUrl', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mediaId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_GetMemoriesUrl clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_GetMemoriesUrl copyWith( + void Function(ApplicationData_GetMemoriesUrl) updates) => + super.copyWith( + (message) => updates(message as ApplicationData_GetMemoriesUrl)) + as ApplicationData_GetMemoriesUrl; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_GetMemoriesUrl create() => + ApplicationData_GetMemoriesUrl._(); + @$core.override + ApplicationData_GetMemoriesUrl createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_GetMemoriesUrl getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_GetMemoriesUrl? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => $_clearField(1); +} + +class ApplicationData_GetMemoriesUsage extends $pb.GeneratedMessage { + factory ApplicationData_GetMemoriesUsage() => create(); + + ApplicationData_GetMemoriesUsage._(); + + factory ApplicationData_GetMemoriesUsage.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_GetMemoriesUsage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.GetMemoriesUsage', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_GetMemoriesUsage clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_GetMemoriesUsage copyWith( + void Function(ApplicationData_GetMemoriesUsage) updates) => + super.copyWith( + (message) => updates(message as ApplicationData_GetMemoriesUsage)) + as ApplicationData_GetMemoriesUsage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_GetMemoriesUsage create() => + ApplicationData_GetMemoriesUsage._(); + @$core.override + ApplicationData_GetMemoriesUsage createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_GetMemoriesUsage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static ApplicationData_GetMemoriesUsage? _defaultInstance; +} + enum ApplicationData_ApplicationData { textMessage, getUserByUsername, @@ -2741,6 +3046,11 @@ enum ApplicationData_ApplicationData { setLoginToken, registerPasswordlessRecovery, passwordlessNotification, + requestMemoriesUpload, + confirmMemoriesUpload, + getMemoriesList, + getMemoriesUrl, + getMemoriesUsage, notSet } @@ -2774,6 +3084,11 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_SetLoginToken? setLoginToken, ApplicationData_RegisterPasswordLessRecovery? registerPasswordlessRecovery, ApplicationData_PasswordlessNotification? passwordlessNotification, + ApplicationData_RequestMemoriesUpload? requestMemoriesUpload, + ApplicationData_ConfirmMemoriesUpload? confirmMemoriesUpload, + ApplicationData_GetMemoriesList? getMemoriesList, + ApplicationData_GetMemoriesUrl? getMemoriesUrl, + ApplicationData_GetMemoriesUsage? getMemoriesUsage, }) { final result = create(); if (textMessage != null) result.textMessage = textMessage; @@ -2812,6 +3127,13 @@ class ApplicationData extends $pb.GeneratedMessage { result.registerPasswordlessRecovery = registerPasswordlessRecovery; if (passwordlessNotification != null) result.passwordlessNotification = passwordlessNotification; + if (requestMemoriesUpload != null) + result.requestMemoriesUpload = requestMemoriesUpload; + if (confirmMemoriesUpload != null) + result.confirmMemoriesUpload = confirmMemoriesUpload; + if (getMemoriesList != null) result.getMemoriesList = getMemoriesList; + if (getMemoriesUrl != null) result.getMemoriesUrl = getMemoriesUrl; + if (getMemoriesUsage != null) result.getMemoriesUsage = getMemoriesUsage; return result; } @@ -2854,6 +3176,11 @@ class ApplicationData extends $pb.GeneratedMessage { 30: ApplicationData_ApplicationData.setLoginToken, 31: ApplicationData_ApplicationData.registerPasswordlessRecovery, 32: ApplicationData_ApplicationData.passwordlessNotification, + 33: ApplicationData_ApplicationData.requestMemoriesUpload, + 34: ApplicationData_ApplicationData.confirmMemoriesUpload, + 35: ApplicationData_ApplicationData.getMemoriesList, + 36: ApplicationData_ApplicationData.getMemoriesUrl, + 37: ApplicationData_ApplicationData.getMemoriesUsage, 0: ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -2889,7 +3216,12 @@ class ApplicationData extends $pb.GeneratedMessage { 29, 30, 31, - 32 + 32, + 33, + 34, + 35, + 36, + 37 ]) ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', @@ -2987,6 +3319,21 @@ class ApplicationData extends $pb.GeneratedMessage { ..aOM( 32, _omitFieldNames ? '' : 'passwordlessNotification', subBuilder: ApplicationData_PasswordlessNotification.create) + ..aOM( + 33, _omitFieldNames ? '' : 'requestMemoriesUpload', + subBuilder: ApplicationData_RequestMemoriesUpload.create) + ..aOM( + 34, _omitFieldNames ? '' : 'confirmMemoriesUpload', + subBuilder: ApplicationData_ConfirmMemoriesUpload.create) + ..aOM( + 35, _omitFieldNames ? '' : 'getMemoriesList', + subBuilder: ApplicationData_GetMemoriesList.create) + ..aOM( + 36, _omitFieldNames ? '' : 'getMemoriesUrl', + subBuilder: ApplicationData_GetMemoriesUrl.create) + ..aOM( + 37, _omitFieldNames ? '' : 'getMemoriesUsage', + subBuilder: ApplicationData_GetMemoriesUsage.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -3036,6 +3383,11 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(30) @$pb.TagNumber(31) @$pb.TagNumber(32) + @$pb.TagNumber(33) + @$pb.TagNumber(34) + @$pb.TagNumber(35) + @$pb.TagNumber(36) + @$pb.TagNumber(37) ApplicationData_ApplicationData whichApplicationData() => _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -3066,6 +3418,11 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(30) @$pb.TagNumber(31) @$pb.TagNumber(32) + @$pb.TagNumber(33) + @$pb.TagNumber(34) + @$pb.TagNumber(35) + @$pb.TagNumber(36) + @$pb.TagNumber(37) void clearApplicationData() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -3401,6 +3758,68 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(32) ApplicationData_PasswordlessNotification ensurePasswordlessNotification() => $_ensure(27); + + @$pb.TagNumber(33) + ApplicationData_RequestMemoriesUpload get requestMemoriesUpload => $_getN(28); + @$pb.TagNumber(33) + set requestMemoriesUpload(ApplicationData_RequestMemoriesUpload value) => + $_setField(33, value); + @$pb.TagNumber(33) + $core.bool hasRequestMemoriesUpload() => $_has(28); + @$pb.TagNumber(33) + void clearRequestMemoriesUpload() => $_clearField(33); + @$pb.TagNumber(33) + ApplicationData_RequestMemoriesUpload ensureRequestMemoriesUpload() => + $_ensure(28); + + @$pb.TagNumber(34) + ApplicationData_ConfirmMemoriesUpload get confirmMemoriesUpload => $_getN(29); + @$pb.TagNumber(34) + set confirmMemoriesUpload(ApplicationData_ConfirmMemoriesUpload value) => + $_setField(34, value); + @$pb.TagNumber(34) + $core.bool hasConfirmMemoriesUpload() => $_has(29); + @$pb.TagNumber(34) + void clearConfirmMemoriesUpload() => $_clearField(34); + @$pb.TagNumber(34) + ApplicationData_ConfirmMemoriesUpload ensureConfirmMemoriesUpload() => + $_ensure(29); + + @$pb.TagNumber(35) + ApplicationData_GetMemoriesList get getMemoriesList => $_getN(30); + @$pb.TagNumber(35) + set getMemoriesList(ApplicationData_GetMemoriesList value) => + $_setField(35, value); + @$pb.TagNumber(35) + $core.bool hasGetMemoriesList() => $_has(30); + @$pb.TagNumber(35) + void clearGetMemoriesList() => $_clearField(35); + @$pb.TagNumber(35) + ApplicationData_GetMemoriesList ensureGetMemoriesList() => $_ensure(30); + + @$pb.TagNumber(36) + ApplicationData_GetMemoriesUrl get getMemoriesUrl => $_getN(31); + @$pb.TagNumber(36) + set getMemoriesUrl(ApplicationData_GetMemoriesUrl value) => + $_setField(36, value); + @$pb.TagNumber(36) + $core.bool hasGetMemoriesUrl() => $_has(31); + @$pb.TagNumber(36) + void clearGetMemoriesUrl() => $_clearField(36); + @$pb.TagNumber(36) + ApplicationData_GetMemoriesUrl ensureGetMemoriesUrl() => $_ensure(31); + + @$pb.TagNumber(37) + ApplicationData_GetMemoriesUsage get getMemoriesUsage => $_getN(32); + @$pb.TagNumber(37) + set getMemoriesUsage(ApplicationData_GetMemoriesUsage value) => + $_setField(37, value); + @$pb.TagNumber(37) + $core.bool hasGetMemoriesUsage() => $_has(32); + @$pb.TagNumber(37) + void clearGetMemoriesUsage() => $_clearField(37); + @$pb.TagNumber(37) + ApplicationData_GetMemoriesUsage ensureGetMemoriesUsage() => $_ensure(32); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index 56e88824..8aa205e1 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -733,6 +733,51 @@ const ApplicationData$json = { '9': 0, '10': 'passwordlessNotification' }, + { + '1': 'request_memories_upload', + '3': 33, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.RequestMemoriesUpload', + '9': 0, + '10': 'requestMemoriesUpload' + }, + { + '1': 'confirm_memories_upload', + '3': 34, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.ConfirmMemoriesUpload', + '9': 0, + '10': 'confirmMemoriesUpload' + }, + { + '1': 'get_memories_list', + '3': 35, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.GetMemoriesList', + '9': 0, + '10': 'getMemoriesList' + }, + { + '1': 'get_memories_url', + '3': 36, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.GetMemoriesUrl', + '9': 0, + '10': 'getMemoriesUrl' + }, + { + '1': 'get_memories_usage', + '3': 37, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.GetMemoriesUsage', + '9': 0, + '10': 'getMemoriesUsage' + }, ], '3': [ ApplicationData_TextMessage$json, @@ -756,7 +801,12 @@ const ApplicationData$json = { ApplicationData_SetLoginToken$json, ApplicationData_Deprecated$json, ApplicationData_RegisterPasswordLessRecovery$json, - ApplicationData_PasswordlessNotification$json + ApplicationData_PasswordlessNotification$json, + ApplicationData_RequestMemoriesUpload$json, + ApplicationData_ConfirmMemoriesUpload$json, + ApplicationData_GetMemoriesList$json, + ApplicationData_GetMemoriesUrl$json, + ApplicationData_GetMemoriesUsage$json ], '8': [ {'1': 'ApplicationData'}, @@ -976,6 +1026,45 @@ const ApplicationData_PasswordlessNotification$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_RequestMemoriesUpload$json = { + '1': 'RequestMemoriesUpload', + '2': [ + {'1': 'size', '3': 1, '4': 1, '5': 3, '10': 'size'}, + {'1': 'original_date', '3': 2, '4': 1, '5': 3, '10': 'originalDate'}, + ], +}; + +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_ConfirmMemoriesUpload$json = { + '1': 'ConfirmMemoriesUpload', + '2': [ + {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, + ], +}; + +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_GetMemoriesList$json = { + '1': 'GetMemoriesList', + '2': [ + {'1': 'offset_date', '3': 1, '4': 1, '5': 3, '10': 'offsetDate'}, + {'1': 'limit', '3': 2, '4': 1, '5': 3, '10': 'limit'}, + ], +}; + +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_GetMemoriesUrl$json = { + '1': 'GetMemoriesUrl', + '2': [ + {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, + ], +}; + +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_GetMemoriesUsage$json = { + '1': 'GetMemoriesUsage', +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' @@ -1026,31 +1115,45 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'lvbkRhdGEuUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeUgAUhxyZWdpc3RlclBhc3N3b3Jk' 'bGVzc1JlY292ZXJ5EnkKGXBhc3N3b3JkbGVzc19ub3RpZmljYXRpb24YICABKAsyOi5jbGllbn' 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIY' - 'cGFzc3dvcmRsZXNzTm90aWZpY2F0aW9uGmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKA' - 'NSBnVzZXJJZBISCgRib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNo' - 'RGF0YYgBAUIMCgpfcHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA' - 'EgASgJUgh1c2VybmFtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNl' - 'cm5hbWUaNQoUVXBkYXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2' - 'xlRmNtGiYKC0dldFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFp' - 'bGFibGVQbGFucxoXChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5Jbm' - 'ZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoS' - 'R2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZW' - 'RQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25l' - 'ZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaW' - 'duZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1' - 'cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bm' - 'xvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRl' - 'ZF91c2VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbh' - 'pxCgtJUEFQdXJjaGFzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNl' - 'GAIgASgJUgZzb3VyY2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbk' - 'RhdGEaDwoNSVBBRm9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxV' - 'c2VyEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3' - 'Rva2VuGAEgASgMUgpsb2dpblRva2VuGgwKCkRlcHJlY2F0ZWQajgEKHFJlZ2lzdGVyUGFzc3dv' - 'cmRMZXNzUmVjb3ZlcnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeXB0ZWRTZX' - 'J2ZXJLZXkSKwoOcGluVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6IAQFCEQoP' - 'X3BpblVubG9ja1Rva2VuGnAKGFBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYX' - 'Rpb25faWQYASABKAlSDm5vdGlmaWNhdGlvbklkEisKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgM' - 'UhBlbmNyeXB0ZWRNZXNzYWdlQhEKD0FwcGxpY2F0aW9uRGF0YQ=='); + 'cGFzc3dvcmRsZXNzTm90aWZpY2F0aW9uEnEKF3JlcXVlc3RfbWVtb3JpZXNfdXBsb2FkGCEgAS' + 'gLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVxdWVzdE1lbW9yaWVzVXBs' + 'b2FkSABSFXJlcXVlc3RNZW1vcmllc1VwbG9hZBJxChdjb25maXJtX21lbW9yaWVzX3VwbG9hZB' + 'giIAEoCzI3LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNvbmZpcm1NZW1vcmll' + 'c1VwbG9hZEgAUhVjb25maXJtTWVtb3JpZXNVcGxvYWQSXwoRZ2V0X21lbW9yaWVzX2xpc3QYIy' + 'ABKAsyMS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc0xpc3RI' + 'AFIPZ2V0TWVtb3JpZXNMaXN0ElwKEGdldF9tZW1vcmllc191cmwYJCABKAsyMC5jbGllbnRfdG' + '9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc1VybEgAUg5nZXRNZW1vcmllc1Vy' + 'bBJiChJnZXRfbWVtb3JpZXNfdXNhZ2UYJSABKAsyMi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2' + 'F0aW9uRGF0YS5HZXRNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2UaagoLVGV4dE1l' + 'c3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZHkSIAoJcH' + 'VzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0VXNlckJ5' + 'VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEh' + 'oKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpn' + 'b29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIA' + 'EoA1IGdXNlcklkGhMKEUdldEF2YWlsYWJsZVBsYW5zGhcKFUdldEFkZEFjY291bnRzSW52aXRl' + 'cxoVChNHZXRDdXJyZW50UGxhbkluZm9zGi8KFFJlbW92ZUFkZGl0aW9uYWxVc2VyEhcKB3VzZX' + 'JfaWQYASABKANSBnVzZXJJZBotChJHZXRQcmVrZXlzQnlVc2VySWQSFwoHdXNlcl9pZBgBIAEo' + 'A1IGdXNlcklkGjIKF0dldFNpZ25lZFByZUtleUJ5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBn' + 'VzZXJJZBqbAQoSVXBkYXRlU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKANS' + 'DnNpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2Ch' + 'dzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGjUK' + 'DERvd25sb2FkRG9uZRIlCg5kb3dubG9hZF90b2tlbhgBIAEoDFINZG93bmxvYWRUb2tlbhpOCg' + 'pSZXBvcnRVc2VyEigKEHJlcG9ydGVkX3VzZXJfaWQYASABKANSDnJlcG9ydGVkVXNlcklkEhYK' + 'BnJlYXNvbhgCIAEoCVIGcmVhc29uGnEKC0lQQVB1cmNoYXNlEh0KCnByb2R1Y3RfaWQYASABKA' + 'lSCXByb2R1Y3RJZBIWCgZzb3VyY2UYAiABKAlSBnNvdXJjZRIrChF2ZXJpZmljYXRpb25fZGF0' + 'YRgDIAEoCVIQdmVyaWZpY2F0aW9uRGF0YRoPCg1JUEFGb3JjZUNoZWNrGg8KDURlbGV0ZUFjY2' + '91bnQaLAoRQWRkQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGjAKDVNl' + 'dExvZ2luVG9rZW4SHwoLbG9naW5fdG9rZW4YASABKAxSCmxvZ2luVG9rZW4aDAoKRGVwcmVjYX' + 'RlZBqOAQocUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeRIuChJlbmNyeXB0ZWRTZXJ2ZXJL' + 'ZXkYASABKAxSEmVuY3J5cHRlZFNlcnZlcktleRIrCg5waW5VbmxvY2tUb2tlbhgCIAEoDEgAUg' + '5waW5VbmxvY2tUb2tlbogBAUIRCg9fcGluVW5sb2NrVG9rZW4acAoYUGFzc3dvcmRsZXNzTm90' + 'aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSKwoRZW' + '5jcnlwdGVkX21lc3NhZ2UYAiABKAxSEGVuY3J5cHRlZE1lc3NhZ2UaUAoVUmVxdWVzdE1lbW9y' + 'aWVzVXBsb2FkEhIKBHNpemUYASABKANSBHNpemUSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3' + 'JpZ2luYWxEYXRlGjIKFUNvbmZpcm1NZW1vcmllc1VwbG9hZBIZCghtZWRpYV9pZBgBIAEoCVIH' + 'bWVkaWFJZBpICg9HZXRNZW1vcmllc0xpc3QSHwoLb2Zmc2V0X2RhdGUYASABKANSCm9mZnNldE' + 'RhdGUSFAoFbGltaXQYAiABKANSBWxpbWl0GisKDkdldE1lbW9yaWVzVXJsEhkKCG1lZGlhX2lk' + 'GAEgASgJUgdtZWRpYUlkGhIKEEdldE1lbW9yaWVzVXNhZ2VCEQoPQXBwbGljYXRpb25EYXRh'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index fdeb45a6..49f1bd7c 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -430,6 +430,7 @@ class Response_Plan extends $pb.GeneratedMessage { $fixnum.Int64? yearlyCostsCent, $core.bool? allowedToSendTextMessages, $core.bool? isAdditionalAccount, + $fixnum.Int64? memoriesSizeLimit, }) { final result = create(); if (planId != null) result.planId = planId; @@ -447,6 +448,7 @@ class Response_Plan extends $pb.GeneratedMessage { result.allowedToSendTextMessages = allowedToSendTextMessages; if (isAdditionalAccount != null) result.isAdditionalAccount = isAdditionalAccount; + if (memoriesSizeLimit != null) result.memoriesSizeLimit = memoriesSizeLimit; return result; } @@ -473,6 +475,7 @@ class Response_Plan extends $pb.GeneratedMessage { ..aInt64(8, _omitFieldNames ? '' : 'yearlyCostsCent') ..aOB(9, _omitFieldNames ? '' : 'allowedToSendTextMessages') ..aOB(10, _omitFieldNames ? '' : 'isAdditionalAccount') + ..aInt64(11, _omitFieldNames ? '' : 'memoriesSizeLimit') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -575,6 +578,15 @@ class Response_Plan extends $pb.GeneratedMessage { $core.bool hasIsAdditionalAccount() => $_has(8); @$pb.TagNumber(10) void clearIsAdditionalAccount() => $_clearField(10); + + @$pb.TagNumber(11) + $fixnum.Int64 get memoriesSizeLimit => $_getI64(9); + @$pb.TagNumber(11) + set memoriesSizeLimit($fixnum.Int64 value) => $_setInt64(9, value); + @$pb.TagNumber(11) + $core.bool hasMemoriesSizeLimit() => $_has(9); + @$pb.TagNumber(11) + void clearMemoriesSizeLimit() => $_clearField(11); } class Response_Plans extends $pb.GeneratedMessage { @@ -1635,6 +1647,355 @@ class Response_PasswordlessNotificationMessages extends $pb.GeneratedMessage { $_getList(0); } +class Response_MemoriesUploadUrls extends $pb.GeneratedMessage { + factory Response_MemoriesUploadUrls({ + $core.String? mediaId, + $core.String? thumbnailUploadUrl, + $core.String? fullUploadUrl, + }) { + final result = create(); + if (mediaId != null) result.mediaId = mediaId; + if (thumbnailUploadUrl != null) + result.thumbnailUploadUrl = thumbnailUploadUrl; + if (fullUploadUrl != null) result.fullUploadUrl = fullUploadUrl; + return result; + } + + Response_MemoriesUploadUrls._(); + + factory Response_MemoriesUploadUrls.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_MemoriesUploadUrls.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.MemoriesUploadUrls', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mediaId') + ..aOS(2, _omitFieldNames ? '' : 'thumbnailUploadUrl') + ..aOS(3, _omitFieldNames ? '' : 'fullUploadUrl') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesUploadUrls clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesUploadUrls copyWith( + void Function(Response_MemoriesUploadUrls) updates) => + super.copyWith( + (message) => updates(message as Response_MemoriesUploadUrls)) + as Response_MemoriesUploadUrls; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_MemoriesUploadUrls create() => + Response_MemoriesUploadUrls._(); + @$core.override + Response_MemoriesUploadUrls createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_MemoriesUploadUrls getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_MemoriesUploadUrls? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get thumbnailUploadUrl => $_getSZ(1); + @$pb.TagNumber(2) + set thumbnailUploadUrl($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasThumbnailUploadUrl() => $_has(1); + @$pb.TagNumber(2) + void clearThumbnailUploadUrl() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get fullUploadUrl => $_getSZ(2); + @$pb.TagNumber(3) + set fullUploadUrl($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasFullUploadUrl() => $_has(2); + @$pb.TagNumber(3) + void clearFullUploadUrl() => $_clearField(3); +} + +class Response_MediaItem extends $pb.GeneratedMessage { + factory Response_MediaItem({ + $core.String? mediaId, + $fixnum.Int64? originalDate, + $core.String? thumbnailDownloadUrl, + }) { + final result = create(); + if (mediaId != null) result.mediaId = mediaId; + if (originalDate != null) result.originalDate = originalDate; + if (thumbnailDownloadUrl != null) + result.thumbnailDownloadUrl = thumbnailDownloadUrl; + return result; + } + + Response_MediaItem._(); + + factory Response_MediaItem.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_MediaItem.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.MediaItem', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mediaId') + ..aInt64(2, _omitFieldNames ? '' : 'originalDate') + ..aOS(3, _omitFieldNames ? '' : 'thumbnailDownloadUrl') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MediaItem clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MediaItem copyWith(void Function(Response_MediaItem) updates) => + super.copyWith((message) => updates(message as Response_MediaItem)) + as Response_MediaItem; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_MediaItem create() => Response_MediaItem._(); + @$core.override + Response_MediaItem createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_MediaItem getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_MediaItem? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get originalDate => $_getI64(1); + @$pb.TagNumber(2) + set originalDate($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasOriginalDate() => $_has(1); + @$pb.TagNumber(2) + void clearOriginalDate() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get thumbnailDownloadUrl => $_getSZ(2); + @$pb.TagNumber(3) + set thumbnailDownloadUrl($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasThumbnailDownloadUrl() => $_has(2); + @$pb.TagNumber(3) + void clearThumbnailDownloadUrl() => $_clearField(3); +} + +class Response_MemoriesList extends $pb.GeneratedMessage { + factory Response_MemoriesList({ + $core.Iterable? items, + }) { + final result = create(); + if (items != null) result.items.addAll(items); + return result; + } + + Response_MemoriesList._(); + + factory Response_MemoriesList.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_MemoriesList.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.MemoriesList', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..pPM(1, _omitFieldNames ? '' : 'items', + subBuilder: Response_MediaItem.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesList clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesList copyWith( + void Function(Response_MemoriesList) updates) => + super.copyWith((message) => updates(message as Response_MemoriesList)) + as Response_MemoriesList; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_MemoriesList create() => Response_MemoriesList._(); + @$core.override + Response_MemoriesList createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_MemoriesList getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_MemoriesList? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList get items => $_getList(0); +} + +class Response_MemoriesUrl extends $pb.GeneratedMessage { + factory Response_MemoriesUrl({ + $core.String? fullDownloadUrl, + }) { + final result = create(); + if (fullDownloadUrl != null) result.fullDownloadUrl = fullDownloadUrl; + return result; + } + + Response_MemoriesUrl._(); + + factory Response_MemoriesUrl.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_MemoriesUrl.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.MemoriesUrl', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'fullDownloadUrl') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesUrl clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesUrl copyWith(void Function(Response_MemoriesUrl) updates) => + super.copyWith((message) => updates(message as Response_MemoriesUrl)) + as Response_MemoriesUrl; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_MemoriesUrl create() => Response_MemoriesUrl._(); + @$core.override + Response_MemoriesUrl createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_MemoriesUrl getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_MemoriesUrl? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get fullDownloadUrl => $_getSZ(0); + @$pb.TagNumber(1) + set fullDownloadUrl($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasFullDownloadUrl() => $_has(0); + @$pb.TagNumber(1) + void clearFullDownloadUrl() => $_clearField(1); +} + +class Response_MemoriesUsage extends $pb.GeneratedMessage { + factory Response_MemoriesUsage({ + $fixnum.Int64? maxBytes, + $fixnum.Int64? currentBytes, + $fixnum.Int64? count, + }) { + final result = create(); + if (maxBytes != null) result.maxBytes = maxBytes; + if (currentBytes != null) result.currentBytes = currentBytes; + if (count != null) result.count = count; + return result; + } + + Response_MemoriesUsage._(); + + factory Response_MemoriesUsage.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_MemoriesUsage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.MemoriesUsage', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'maxBytes') + ..aInt64(2, _omitFieldNames ? '' : 'currentBytes') + ..aInt64(3, _omitFieldNames ? '' : 'count') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesUsage clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_MemoriesUsage copyWith( + void Function(Response_MemoriesUsage) updates) => + super.copyWith((message) => updates(message as Response_MemoriesUsage)) + as Response_MemoriesUsage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_MemoriesUsage create() => Response_MemoriesUsage._(); + @$core.override + Response_MemoriesUsage createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_MemoriesUsage getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_MemoriesUsage? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get maxBytes => $_getI64(0); + @$pb.TagNumber(1) + set maxBytes($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasMaxBytes() => $_has(0); + @$pb.TagNumber(1) + void clearMaxBytes() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get currentBytes => $_getI64(1); + @$pb.TagNumber(2) + set currentBytes($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasCurrentBytes() => $_has(1); + @$pb.TagNumber(2) + void clearCurrentBytes() => $_clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get count => $_getI64(2); + @$pb.TagNumber(3) + set count($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasCount() => $_has(2); + @$pb.TagNumber(3) + void clearCount() => $_clearField(3); +} + enum Response_Ok_Ok { none, userid, @@ -1653,6 +2014,10 @@ enum Response_Ok_Ok { proofOfWork, passwordlessRecoveryServerKey, passwordlessNotificationMessages, + memoriesUploadUrls, + memoriesList, + memoriesUrl, + memoriesUsage, notSet } @@ -1675,6 +2040,10 @@ class Response_Ok extends $pb.GeneratedMessage { Response_ProofOfWork? proofOfWork, $core.List<$core.int>? passwordlessRecoveryServerKey, Response_PasswordlessNotificationMessages? passwordlessNotificationMessages, + Response_MemoriesUploadUrls? memoriesUploadUrls, + Response_MemoriesList? memoriesList, + Response_MemoriesUrl? memoriesUrl, + Response_MemoriesUsage? memoriesUsage, }) { final result = create(); if (none != null) result.none = none; @@ -1698,6 +2067,11 @@ class Response_Ok extends $pb.GeneratedMessage { if (passwordlessNotificationMessages != null) result.passwordlessNotificationMessages = passwordlessNotificationMessages; + if (memoriesUploadUrls != null) + result.memoriesUploadUrls = memoriesUploadUrls; + if (memoriesList != null) result.memoriesList = memoriesList; + if (memoriesUrl != null) result.memoriesUrl = memoriesUrl; + if (memoriesUsage != null) result.memoriesUsage = memoriesUsage; return result; } @@ -1728,6 +2102,10 @@ class Response_Ok extends $pb.GeneratedMessage { 15: Response_Ok_Ok.proofOfWork, 16: Response_Ok_Ok.passwordlessRecoveryServerKey, 17: Response_Ok_Ok.passwordlessNotificationMessages, + 18: Response_Ok_Ok.memoriesUploadUrls, + 19: Response_Ok_Ok.memoriesList, + 20: Response_Ok_Ok.memoriesUrl, + 21: Response_Ok_Ok.memoriesUsage, 0: Response_Ok_Ok.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -1735,7 +2113,29 @@ class Response_Ok extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]) + ..oo(0, [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21 + ]) ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aInt64(2, _omitFieldNames ? '' : 'userid') ..a<$core.List<$core.int>>( @@ -1772,6 +2172,15 @@ class Response_Ok extends $pb.GeneratedMessage { ..aOM( 17, _omitFieldNames ? '' : 'passwordlessNotificationMessages', subBuilder: Response_PasswordlessNotificationMessages.create) + ..aOM( + 18, _omitFieldNames ? '' : 'memoriesUploadUrls', + subBuilder: Response_MemoriesUploadUrls.create) + ..aOM(19, _omitFieldNames ? '' : 'memoriesList', + subBuilder: Response_MemoriesList.create) + ..aOM(20, _omitFieldNames ? '' : 'memoriesUrl', + subBuilder: Response_MemoriesUrl.create) + ..aOM(21, _omitFieldNames ? '' : 'memoriesUsage', + subBuilder: Response_MemoriesUsage.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1810,6 +2219,10 @@ class Response_Ok extends $pb.GeneratedMessage { @$pb.TagNumber(15) @$pb.TagNumber(16) @$pb.TagNumber(17) + @$pb.TagNumber(18) + @$pb.TagNumber(19) + @$pb.TagNumber(20) + @$pb.TagNumber(21) Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @$pb.TagNumber(2) @@ -1828,6 +2241,10 @@ class Response_Ok extends $pb.GeneratedMessage { @$pb.TagNumber(15) @$pb.TagNumber(16) @$pb.TagNumber(17) + @$pb.TagNumber(18) + @$pb.TagNumber(19) + @$pb.TagNumber(20) + @$pb.TagNumber(21) void clearOk() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -2012,6 +2429,51 @@ class Response_Ok extends $pb.GeneratedMessage { @$pb.TagNumber(17) Response_PasswordlessNotificationMessages ensurePasswordlessNotificationMessages() => $_ensure(16); + + @$pb.TagNumber(18) + Response_MemoriesUploadUrls get memoriesUploadUrls => $_getN(17); + @$pb.TagNumber(18) + set memoriesUploadUrls(Response_MemoriesUploadUrls value) => + $_setField(18, value); + @$pb.TagNumber(18) + $core.bool hasMemoriesUploadUrls() => $_has(17); + @$pb.TagNumber(18) + void clearMemoriesUploadUrls() => $_clearField(18); + @$pb.TagNumber(18) + Response_MemoriesUploadUrls ensureMemoriesUploadUrls() => $_ensure(17); + + @$pb.TagNumber(19) + Response_MemoriesList get memoriesList => $_getN(18); + @$pb.TagNumber(19) + set memoriesList(Response_MemoriesList value) => $_setField(19, value); + @$pb.TagNumber(19) + $core.bool hasMemoriesList() => $_has(18); + @$pb.TagNumber(19) + void clearMemoriesList() => $_clearField(19); + @$pb.TagNumber(19) + Response_MemoriesList ensureMemoriesList() => $_ensure(18); + + @$pb.TagNumber(20) + Response_MemoriesUrl get memoriesUrl => $_getN(19); + @$pb.TagNumber(20) + set memoriesUrl(Response_MemoriesUrl value) => $_setField(20, value); + @$pb.TagNumber(20) + $core.bool hasMemoriesUrl() => $_has(19); + @$pb.TagNumber(20) + void clearMemoriesUrl() => $_clearField(20); + @$pb.TagNumber(20) + Response_MemoriesUrl ensureMemoriesUrl() => $_ensure(19); + + @$pb.TagNumber(21) + Response_MemoriesUsage get memoriesUsage => $_getN(20); + @$pb.TagNumber(21) + set memoriesUsage(Response_MemoriesUsage value) => $_setField(21, value); + @$pb.TagNumber(21) + $core.bool hasMemoriesUsage() => $_has(20); + @$pb.TagNumber(21) + void clearMemoriesUsage() => $_clearField(21); + @$pb.TagNumber(21) + Response_MemoriesUsage ensureMemoriesUsage() => $_ensure(20); } enum Response_Response { ok, error, notSet } diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index 5a400286..dd2041f9 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -178,6 +178,11 @@ const Response$json = { Response_ProofOfWork$json, Response_PasswordlessNotificationMessage$json, Response_PasswordlessNotificationMessages$json, + Response_MemoriesUploadUrls$json, + Response_MediaItem$json, + Response_MemoriesList$json, + Response_MemoriesUrl$json, + Response_MemoriesUsage$json, Response_Ok$json ], '8': [ @@ -242,6 +247,13 @@ const Response_Plan$json = { '5': 8, '10': 'isAdditionalAccount' }, + { + '1': 'memories_size_limit', + '3': 11, + '4': 1, + '5': 3, + '10': 'memoriesSizeLimit' + }, ], }; @@ -541,6 +553,71 @@ const Response_PasswordlessNotificationMessages$json = { ], }; +@$core.Deprecated('Use responseDescriptor instead') +const Response_MemoriesUploadUrls$json = { + '1': 'MemoriesUploadUrls', + '2': [ + {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, + { + '1': 'thumbnail_upload_url', + '3': 2, + '4': 1, + '5': 9, + '10': 'thumbnailUploadUrl' + }, + {'1': 'full_upload_url', '3': 3, '4': 1, '5': 9, '10': 'fullUploadUrl'}, + ], +}; + +@$core.Deprecated('Use responseDescriptor instead') +const Response_MediaItem$json = { + '1': 'MediaItem', + '2': [ + {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, + {'1': 'original_date', '3': 2, '4': 1, '5': 3, '10': 'originalDate'}, + { + '1': 'thumbnail_download_url', + '3': 3, + '4': 1, + '5': 9, + '10': 'thumbnailDownloadUrl' + }, + ], +}; + +@$core.Deprecated('Use responseDescriptor instead') +const Response_MemoriesList$json = { + '1': 'MemoriesList', + '2': [ + { + '1': 'items', + '3': 1, + '4': 3, + '5': 11, + '6': '.server_to_client.Response.MediaItem', + '10': 'items' + }, + ], +}; + +@$core.Deprecated('Use responseDescriptor instead') +const Response_MemoriesUrl$json = { + '1': 'MemoriesUrl', + '2': [ + {'1': 'full_download_url', '3': 1, '4': 1, '5': 9, '10': 'fullDownloadUrl'}, + ], +}; + +@$core.Deprecated('Use responseDescriptor instead') +const Response_MemoriesUsage$json = { + '1': 'MemoriesUsage', + '2': [ + {'1': 'max_bytes', '3': 1, '4': 1, '5': 3, '10': 'maxBytes'}, + {'1': 'current_bytes', '3': 2, '4': 1, '5': 3, '10': 'currentBytes'}, + {'1': 'count', '3': 3, '4': 1, '5': 3, '10': 'count'}, + ], +}; + @$core.Deprecated('Use responseDescriptor instead') const Response_Ok$json = { '1': 'Ok', @@ -672,6 +749,42 @@ const Response_Ok$json = { '9': 0, '10': 'passwordlessNotificationMessages' }, + { + '1': 'memories_upload_urls', + '3': 18, + '4': 1, + '5': 11, + '6': '.server_to_client.Response.MemoriesUploadUrls', + '9': 0, + '10': 'memoriesUploadUrls' + }, + { + '1': 'memories_list', + '3': 19, + '4': 1, + '5': 11, + '6': '.server_to_client.Response.MemoriesList', + '9': 0, + '10': 'memoriesList' + }, + { + '1': 'memories_url', + '3': 20, + '4': 1, + '5': 11, + '6': '.server_to_client.Response.MemoriesUrl', + '9': 0, + '10': 'memoriesUrl' + }, + { + '1': 'memories_usage', + '3': 21, + '4': 1, + '5': 11, + '6': '.server_to_client.Response.MemoriesUsage', + '9': 0, + '10': 'memoriesUsage' + }, ], '8': [ {'1': 'Ok'}, @@ -682,7 +795,7 @@ const Response_Ok$json = { final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'CghSZXNwb25zZRIvCgJvaxgBIAEoCzIdLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuT2tIAF' 'ICb2sSKAoFZXJyb3IYAiABKA4yEC5lcnJvci5FcnJvckNvZGVIAFIFZXJyb3IaIwoNQXV0aGVu' - 'dGljYXRlZBISCgRwbGFuGAEgASgJUgRwbGFuGuQDCgRQbGFuEhcKB3BsYW5faWQYASABKAlSBn' + 'dGljYXRlZBISCgRwbGFuGAEgASgJUgRwbGFuGpQECgRQbGFuEhcKB3BsYW5faWQYASABKAlSBn' 'BsYW5JZBIqChF1cGxvYWRfc2l6ZV9saW1pdBgCIAEoA1IPdXBsb2FkU2l6ZUxpbWl0EjcKGGRh' 'aWx5X21lZGlhX3VwbG9hZF9saW1pdBgDIAEoA1IVZGFpbHlNZWRpYVVwbG9hZExpbWl0ElQKKG' '1heGltYWxfdXBsb2FkX3NpemVfb2Zfc2luZ2xlX21lZGlhX3NpemUYBCABKANSIm1heGltYWxV' @@ -691,64 +804,81 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'A1IQbW9udGhseUNvc3RzQ2VudBIqChF5ZWFybHlfY29zdHNfY2VudBgIIAEoA1IPeWVhcmx5Q2' '9zdHNDZW50EkAKHWFsbG93ZWRfdG9fc2VuZF90ZXh0X21lc3NhZ2VzGAkgASgIUhlhbGxvd2Vk' 'VG9TZW5kVGV4dE1lc3NhZ2VzEjIKFWlzX2FkZGl0aW9uYWxfYWNjb3VudBgKIAEoCFITaXNBZG' - 'RpdGlvbmFsQWNjb3VudBo+CgVQbGFucxI1CgVwbGFucxgBIAMoCzIfLnNlcnZlcl90b19jbGll' - 'bnQuUmVzcG9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZB' - 'gBIAEoCVIGcGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFj' - 'Y291bnRzSW52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb2' - '5zZS5BZGRBY2NvdW50c0ludml0ZVIHaW52aXRlcxpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1' - 'c2VyX2lkGAEgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGgwKCkRlcHJlY2' - 'F0ZWQaDQoLVHJhbnNhY3Rpb24alwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlh' - 'X3VwbG9hZF9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3' - 'VwbG9hZF9tZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQS' - 'MwoTcGF5bWVudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCi' - 'BsYXN0X3BheW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERv' - 'bmVVbml4VGltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbG' - 'llbnQuUmVzcG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2Fj' - 'Y291bnRzGAYgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3' - 'VudFISYWRkaXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVu' - 'ZXdhbIgBARJCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb2' - '5hbEFjY291bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3Bh' - 'eW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbm' - 'FsX2FjY291bnRfb3duZXJfaWQaMAoGUHJlS2V5Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkY' - 'AiABKAxSBnByZWtleRqVAQoMU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKA' - 'NSDnNpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2' - 'ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGv' - 'YDCghVc2VyRGF0YRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOwoHcHJla2V5cxgCIAMoCzIh' - 'LnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlS2V5UgdwcmVrZXlzEh8KCHVzZXJuYW1lGA' - 'cgASgMSABSCHVzZXJuYW1liAEBEjMKE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxIAVIRcHVi' - 'bGljSWRlbnRpdHlLZXmIAQESKAoNc2lnbmVkX3ByZWtleRgEIAEoDEgCUgxzaWduZWRQcmVrZX' - 'mIAQESOwoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYBSABKAxIA1IVc2lnbmVkUHJla2V5U2ln' - 'bmF0dXJliAEBEi0KEHNpZ25lZF9wcmVrZXlfaWQYBiABKANIBFIOc2lnbmVkUHJla2V5SWSIAQ' - 'ESLAoPcmVnaXN0cmF0aW9uX2lkGAggASgDSAVSDnJlZ2lzdHJhdGlvbklkiAEBQgsKCV91c2Vy' - 'bmFtZUIWChRfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbm' - 'VkX3ByZWtleV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlv' - 'bl9pZBpZCgtVcGxvYWRUb2tlbhIhCgx1cGxvYWRfdG9rZW4YASABKAxSC3VwbG9hZFRva2VuEi' - 'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl' - 'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2' - '9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1' - 'bHR5Gl4KH1Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEi' - 'sKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3Jk' - 'bGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2' - 'NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdl' - 'cxqwCQoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgKBnVzZXJpZBgCIAEoA0gAUgZ1c2VyaW' - 'QSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2hhbGxlbmdlEkoKC3VwbG9hZHRva2Vu' - 'GAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5VcGxvYWRUb2tlbkgAUgt1cGxvYW' - 'R0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXNl' - 'ckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYgASgMSABSCWF1dGh0b2tlbhJKCgxkZX' - 'ByZWNhdGVkXzcYByABKAsyJS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRlcHJlY2F0ZWRI' - 'AFILZGVwcmVjYXRlZDcSUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNlcnZlcl90b19jbGllbn' - 'QuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEjgKBXBsYW5zGAkgASgL' - 'MiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFucxJNCgxwbGFuYmFsbG' - 'FuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5CYWxsYW5jZUgAUgxw' - 'bGFuYmFsbGFuY2USTAoNZGVwcmVjYXRlZF8xMRgLIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUm' - 'VzcG9uc2UuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESXwoSYWRkYWNjb3VudHNpbnZpdGVz' - 'GAwgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRBY2NvdW50c0ludml0ZXNIAF' - 'ISYWRkYWNjb3VudHNpbnZpdGVzElMKDmRvd25sb2FkdG9rZW5zGA0gASgLMikuc2VydmVyX3Rv' - 'X2NsaWVudC5SZXNwb25zZS5Eb3dubG9hZFRva2Vuc0gAUg5kb3dubG9hZHRva2VucxJNCgxzaW' - 'duZWRwcmVrZXkYDiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlNpZ25lZFByZUtl' - 'eUgAUgxzaWduZWRwcmVrZXkSSgoLcHJvb2ZPZldvcmsYDyABKAsyJi5zZXJ2ZXJfdG9fY2xpZW' - '50LlJlc3BvbnNlLlByb29mT2ZXb3JrSABSC3Byb29mT2ZXb3JrEkkKIHBhc3N3b3JkbGVzc19y' - 'ZWNvdmVyeV9zZXJ2ZXJfa2V5GBAgASgMSABSHXBhc3N3b3JkbGVzc1JlY292ZXJ5U2VydmVyS2' - 'V5EosBCiJwYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uX21lc3NhZ2VzGBEgASgLMjsuc2VydmVy' - 'X3RvX2NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0gAUi' - 'BwYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0IECgJPa0IKCghSZXNwb25zZQ=='); + 'RpdGlvbmFsQWNjb3VudBIuChNtZW1vcmllc19zaXplX2xpbWl0GAsgASgDUhFtZW1vcmllc1Np' + 'emVMaW1pdBo+CgVQbGFucxI1CgVwbGFucxgBIAMoCzIfLnNlcnZlcl90b19jbGllbnQuUmVzcG' + '9uc2UuUGxhblIFcGxhbnMaTQoRQWRkQWNjb3VudHNJbnZpdGUSFwoHcGxhbl9pZBgBIAEoCVIG' + 'cGxhbklkEh8KC2ludml0ZV9jb2RlGAIgASgJUgppbnZpdGVDb2RlGlwKEkFkZEFjY291bnRzSW' + '52aXRlcxJGCgdpbnZpdGVzGAEgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRB' + 'Y2NvdW50c0ludml0ZVIHaW52aXRlcxpFChFBZGRpdGlvbmFsQWNjb3VudBIXCgd1c2VyX2lkGA' + 'EgASgDUgZ1c2VySWQSFwoHcGxhbl9pZBgDIAEoCVIGcGxhbklkGgwKCkRlcHJlY2F0ZWQaDQoL' + 'VHJhbnNhY3Rpb24alwUKDFBsYW5CYWxsYW5jZRJACh11c2VkX2RhaWx5X21lZGlhX3VwbG9hZF' + '9saW1pdBgBIAEoA1IZdXNlZERhaWx5TWVkaWFVcGxvYWRMaW1pdBI+Chx1c2VkX3VwbG9hZF9t' + 'ZWRpYV9zaXplX2xpbWl0GAIgASgDUhh1c2VkVXBsb2FkTWVkaWFTaXplTGltaXQSMwoTcGF5bW' + 'VudF9wZXJpb2RfZGF5cxgDIAEoA0gAUhFwYXltZW50UGVyaW9kRGF5c4gBARJLCiBsYXN0X3Bh' + 'eW1lbnRfZG9uZV91bml4X3RpbWVzdGFtcBgEIAEoA0gBUhxsYXN0UGF5bWVudERvbmVVbml4VG' + 'ltZXN0YW1wiAEBEkoKDHRyYW5zYWN0aW9ucxgFIAMoCzImLnNlcnZlcl90b19jbGllbnQuUmVz' + 'cG9uc2UuVHJhbnNhY3Rpb25SDHRyYW5zYWN0aW9ucxJdChNhZGRpdGlvbmFsX2FjY291bnRzGA' + 'YgAygLMiwuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRpdGlvbmFsQWNjb3VudFISYWRk' + 'aXRpb25hbEFjY291bnRzEiYKDGF1dG9fcmVuZXdhbBgHIAEoCEgCUgthdXRvUmVuZXdhbIgBAR' + 'JCChthZGRpdGlvbmFsX2FjY291bnRfb3duZXJfaWQYCCABKANIA1IYYWRkaXRpb25hbEFjY291' + 'bnRPd25lcklkiAEBQhYKFF9wYXltZW50X3BlcmlvZF9kYXlzQiMKIV9sYXN0X3BheW1lbnRfZG' + '9uZV91bml4X3RpbWVzdGFtcEIPCg1fYXV0b19yZW5ld2FsQh4KHF9hZGRpdGlvbmFsX2FjY291' + 'bnRfb3duZXJfaWQaMAoGUHJlS2V5Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBn' + 'ByZWtleRqVAQoMU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKANSDnNpZ25l' + 'ZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZW' + 'RfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGvYDCghVc2Vy' + 'RGF0YRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOwoHcHJla2V5cxgCIAMoCzIhLnNlcnZlcl' + '90b19jbGllbnQuUmVzcG9uc2UuUHJlS2V5UgdwcmVrZXlzEh8KCHVzZXJuYW1lGAcgASgMSABS' + 'CHVzZXJuYW1liAEBEjMKE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxIAVIRcHVibGljSWRlbn' + 'RpdHlLZXmIAQESKAoNc2lnbmVkX3ByZWtleRgEIAEoDEgCUgxzaWduZWRQcmVrZXmIAQESOwoX' + 'c2lnbmVkX3ByZWtleV9zaWduYXR1cmUYBSABKAxIA1IVc2lnbmVkUHJla2V5U2lnbmF0dXJliA' + 'EBEi0KEHNpZ25lZF9wcmVrZXlfaWQYBiABKANIBFIOc2lnbmVkUHJla2V5SWSIAQESLAoPcmVn' + 'aXN0cmF0aW9uX2lkGAggASgDSAVSDnJlZ2lzdHJhdGlvbklkiAEBQgsKCV91c2VybmFtZUIWCh' + 'RfcHVibGljX2lkZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbmVkX3ByZWtl' + 'eV9zaWduYXR1cmVCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlvbl9pZBpZCg' + 'tVcGxvYWRUb2tlbhIhCgx1cGxvYWRfdG9rZW4YASABKAxSC3VwbG9hZFRva2VuEicKD2Rvd25s' + 'b2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tlbnMSJwoPZG' + '93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV29yaxIWCgZw' + 'cmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1bHR5Gl4KH1' + 'Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEisKEWVuY3J5' + 'cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3JkbGVzc05vdG' + 'lmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2NsaWVudC5S' + 'ZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdlcxqJAQoSTW' + 'Vtb3JpZXNVcGxvYWRVcmxzEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkEjAKFHRodW1ibmFp' + 'bF91cGxvYWRfdXJsGAIgASgJUhJ0aHVtYm5haWxVcGxvYWRVcmwSJgoPZnVsbF91cGxvYWRfdX' + 'JsGAMgASgJUg1mdWxsVXBsb2FkVXJsGoEBCglNZWRpYUl0ZW0SGQoIbWVkaWFfaWQYASABKAlS' + 'B21lZGlhSWQSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3JpZ2luYWxEYXRlEjQKFnRodW1ibm' + 'FpbF9kb3dubG9hZF91cmwYAyABKAlSFHRodW1ibmFpbERvd25sb2FkVXJsGkoKDE1lbW9yaWVz' + 'TGlzdBI6CgVpdGVtcxgBIAMoCzIkLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVkaWFJdG' + 'VtUgVpdGVtcxo5CgtNZW1vcmllc1VybBIqChFmdWxsX2Rvd25sb2FkX3VybBgBIAEoCVIPZnVs' + 'bERvd25sb2FkVXJsGmcKDU1lbW9yaWVzVXNhZ2USGwoJbWF4X2J5dGVzGAEgASgDUghtYXhCeX' + 'RlcxIjCg1jdXJyZW50X2J5dGVzGAIgASgDUgxjdXJyZW50Qnl0ZXMSFAoFY291bnQYAyABKANS' + 'BWNvdW50GoMMCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBn' + 'VzZXJpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2Fk' + 'dG9rZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3' + 'VwbG9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z' + 'ZS5Vc2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEk' + 'oKDGRlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVj' + 'YXRlZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2' + 'NsaWVudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMY' + 'CSABKAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW' + '5iYWxsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNl' + 'SABSDHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaW' + 'VudC5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2lu' + 'dml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aX' + 'Rlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2' + 'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk' + '0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVk' + 'UHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b1' + '9jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmsSSQogcGFzc3dvcmRs' + 'ZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlTZX' + 'J2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRpb25fbWVzc2FnZXMYESABKAsyOy5z' + 'ZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2' + 'VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzEmEKFG1lbW9yaWVzX3VwbG9h' + 'ZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5NZW1vcmllc1VwbG9hZF' + 'VybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9yaWVzX2xpc3QYEyABKAsyJy5zZXJ2' + 'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdEgAUgxtZW1vcmllc0xpc3QSSwoMbW' + 'Vtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5NZW1vcmllc1Vy' + 'bEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZRgVIAEoCzIoLnNlcnZlcl90b19jbG' + 'llbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1vcmllc1VzYWdlQgQKAk9rQgoKCFJl' + 'c3BvbnNl'); From 5258b8f78e2c567d1904bf5b1f01f20efdfd3f31 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 14:25:27 +0200 Subject: [PATCH 13/29] update the protocol --- lib/globals.dart | 2 +- lib/src/model/json/userdata.model.dart | 9 +- lib/src/model/json/userdata.model.g.dart | 9 +- .../api/websocket/client_to_server.pb.dart | 61 +------ .../websocket/client_to_server.pbjson.dart | 86 +++++---- .../generated/passwordless_recovery.pb.dart | 42 ++--- .../passwordless_recovery.pbjson.dart | 29 +-- .../client/passwordless_recovery.proto | 5 +- lib/src/services/api.service.dart | 10 +- lib/src/services/migrations.service.dart | 8 +- .../passwordless_recovery.service.dart | 122 +++++++++---- lib/src/utils/avatars.dart | 45 ++++- .../onboarding/recover_passwordless.view.dart | 27 +-- .../settings.passwordless_recovery.view.dart | 2 +- .../setup.passwordless_recovery.view.dart | 2 +- proto.md | 170 ++++++++++++++++++ test/features/link_parser_test.dart | 2 +- .../passwordless_recovery_service_test.dart | 1 + 18 files changed, 407 insertions(+), 225 deletions(-) create mode 100644 proto.md diff --git a/lib/globals.dart b/lib/globals.dart index 83197b22..4b476b6b 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -35,6 +35,6 @@ class AppState { static bool isInBackgroundTask = false; static bool allowErrorTrackingViaSentry = false; static bool gotMessageFromServer = false; - static int latestAppVersionId = 117; + static int latestAppVersionId = 118; static bool hasCameraPermissions = false; } diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index cf245035..10118bcd 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -217,18 +217,13 @@ class PasswordLessRecovery { // Only stored, so the user can see his deposit email address... String? email; - // <-- // Data shared with trusted friends @JsonKey(defaultValue: 2) int threshold; - // Trusted friends are able to brute-force the pin -> Server delets after X tries - List? pinSeed; + // Used to derive the key from the email/pin + List? serverKeyProtection; List? pinUnlockToken; - - // Stored not on the server, so the server is unable to link a email to a user until the actuall recovery or can - // brute-force the pin - List? encryptedServerKeyNonce; // ---> // Checking with the server that the server data is valid and not delted throug the pin protection for example. diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 13e3565a..b8105cec 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -234,16 +234,12 @@ PasswordLessRecovery _$PasswordLessRecoveryFromJson( Map json, ) => PasswordLessRecovery((json['threshold'] as num?)?.toInt() ?? 2) ..email = json['email'] as String? - ..pinSeed = (json['pinSeed'] as List?) + ..serverKeyProtection = (json['serverKeyProtection'] as List?) ?.map((e) => (e as num).toInt()) .toList() ..pinUnlockToken = (json['pinUnlockToken'] as List?) ?.map((e) => (e as num).toInt()) .toList() - ..encryptedServerKeyNonce = - (json['encryptedServerKeyNonce'] as List?) - ?.map((e) => (e as num).toInt()) - .toList() ..lastServerHeartbeat = json['lastServerHeartbeat'] == null ? null : DateTime.parse(json['lastServerHeartbeat'] as String) @@ -259,9 +255,8 @@ Map _$PasswordLessRecoveryToJson( ) => { 'email': instance.email, 'threshold': instance.threshold, - 'pinSeed': instance.pinSeed, + 'serverKeyProtection': instance.serverKeyProtection, 'pinUnlockToken': instance.pinUnlockToken, - 'encryptedServerKeyNonce': instance.encryptedServerKeyNonce, 'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(), 'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(), 'encryptedServerKey': instance.encryptedServerKey, diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index 7ca11faa..b5ad1689 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -813,15 +813,15 @@ class Handshake_GetServerKeyForPasswordLessRecovery extends $pb.GeneratedMessage { factory Handshake_GetServerKeyForPasswordLessRecovery({ $fixnum.Int64? userId, - $core.List<$core.int>? encryptedServerKeyNone, + $core.List<$core.int>? serverKeyProtection, $core.List<$core.int>? pinUnlockToken, $core.List<$core.int>? pinProtectionKey, $core.String? email, }) { final result = create(); if (userId != null) result.userId = userId; - if (encryptedServerKeyNone != null) - result.encryptedServerKeyNone = encryptedServerKeyNone; + if (serverKeyProtection != null) + result.serverKeyProtection = serverKeyProtection; if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken; if (pinProtectionKey != null) result.pinProtectionKey = pinProtectionKey; if (email != null) result.email = email; @@ -846,7 +846,7 @@ class Handshake_GetServerKeyForPasswordLessRecovery createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'userId') ..a<$core.List<$core.int>>( - 2, _omitFieldNames ? '' : 'encryptedServerKeyNone', $pb.PbFieldType.OY) + 2, _omitFieldNames ? '' : 'serverKeyProtection', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( 3, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( @@ -889,14 +889,13 @@ class Handshake_GetServerKeyForPasswordLessRecovery void clearUserId() => $_clearField(1); @$pb.TagNumber(2) - $core.List<$core.int> get encryptedServerKeyNone => $_getN(1); + $core.List<$core.int> get serverKeyProtection => $_getN(1); @$pb.TagNumber(2) - set encryptedServerKeyNone($core.List<$core.int> value) => - $_setBytes(1, value); + set serverKeyProtection($core.List<$core.int> value) => $_setBytes(1, value); @$pb.TagNumber(2) - $core.bool hasEncryptedServerKeyNone() => $_has(1); + $core.bool hasServerKeyProtection() => $_has(1); @$pb.TagNumber(2) - void clearEncryptedServerKeyNone() => $_clearField(2); + void clearServerKeyProtection() => $_clearField(2); @$pb.TagNumber(3) $core.List<$core.int> get pinUnlockToken => $_getN(2); @@ -1748,50 +1747,6 @@ class ApplicationData_GetAvailablePlans extends $pb.GeneratedMessage { static ApplicationData_GetAvailablePlans? _defaultInstance; } -class ApplicationData_GetAddAccountsInvites extends $pb.GeneratedMessage { - factory ApplicationData_GetAddAccountsInvites() => create(); - - ApplicationData_GetAddAccountsInvites._(); - - factory ApplicationData_GetAddAccountsInvites.fromBuffer( - $core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_GetAddAccountsInvites.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.GetAddAccountsInvites', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_GetAddAccountsInvites clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_GetAddAccountsInvites copyWith( - void Function(ApplicationData_GetAddAccountsInvites) updates) => - super.copyWith((message) => - updates(message as ApplicationData_GetAddAccountsInvites)) - as ApplicationData_GetAddAccountsInvites; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_GetAddAccountsInvites create() => - ApplicationData_GetAddAccountsInvites._(); - @$core.override - ApplicationData_GetAddAccountsInvites createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_GetAddAccountsInvites getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< - ApplicationData_GetAddAccountsInvites>(create); - static ApplicationData_GetAddAccountsInvites? _defaultInstance; -} - class ApplicationData_GetCurrentPlanInfos extends $pb.GeneratedMessage { factory ApplicationData_GetCurrentPlanInfos() => create(); diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index 8aa205e1..92051db5 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -339,11 +339,11 @@ const Handshake_GetServerKeyForPasswordLessRecovery$json = { '2': [ {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, { - '1': 'encrypted_server_key_none', + '1': 'server_key_protection', '3': 2, '4': 1, '5': 12, - '10': 'encryptedServerKeyNone' + '10': 'serverKeyProtection' }, { '1': 'pin_unlock_token', @@ -462,20 +462,20 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'VXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLAoSc2VjcmV0X2xvZ2lu' 'X3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJzaW9uGAMgASgJUgphcH' 'BWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW5fYmFja2dyb3VuZBgF' - 'IAEoCFIMaW5CYWNrZ3JvdW5kGqwCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm' - 'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOQoZZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9u' - 'ZRgCIAEoDFIWZW5jcnlwdGVkU2VydmVyS2V5Tm9uZRItChBwaW5fdW5sb2NrX3Rva2VuGAMgAS' - 'gMSABSDnBpblVubG9ja1Rva2VuiAEBEjEKEnBpbl9wcm90ZWN0aW9uX2tleRgEIAEoDEgBUhBw' - 'aW5Qcm90ZWN0aW9uS2V5iAEBEhkKBWVtYWlsGAUgASgJSAJSBWVtYWlsiAEBQhMKEV9waW5fdW' - '5sb2NrX3Rva2VuQhUKE19waW5fcHJvdGVjdGlvbl9rZXlCCAoGX2VtYWlsGssBCiBSZWdpc3Rl' - 'clBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYXRpb25faWQYASABKAlSDm5vdG' - 'lmaWNhdGlvbklkEi4KE2Rvd25sb2FkX2F1dGhfdG9rZW4YAiABKAxSEWRvd25sb2FkQXV0aFRv' - 'a2VuEhsKCWxhbmdfY29kZRgDIAEoCVIIbGFuZ0NvZGUSIgoKZ29vZ2xlX2ZjbRgEIAEoCUgAUg' - 'lnb29nbGVGY22IAQFCDQoLX2dvb2dsZV9mY20avAEKIENoZWNrRm9yUGFzc3dvcmRsZXNzTm90' - 'aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSLgoTZG' - '93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SPwocYWxyZWFkeV9y' - 'ZWNlaXZlZF9tZXNzYWdlX2lkcxgDIAMoA1IZYWxyZWFkeVJlY2VpdmVkTWVzc2FnZUlkc0ILCg' - 'lIYW5kc2hha2U='); + 'IAEoCFIMaW5CYWNrZ3JvdW5kGqUCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm' + 'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSMgoVc2VydmVyX2tleV9wcm90ZWN0aW9uGAIg' + 'ASgMUhNzZXJ2ZXJLZXlQcm90ZWN0aW9uEi0KEHBpbl91bmxvY2tfdG9rZW4YAyABKAxIAFIOcG' + 'luVW5sb2NrVG9rZW6IAQESMQoScGluX3Byb3RlY3Rpb25fa2V5GAQgASgMSAFSEHBpblByb3Rl' + 'Y3Rpb25LZXmIAQESGQoFZW1haWwYBSABKAlIAlIFZW1haWyIAQFCEwoRX3Bpbl91bmxvY2tfdG' + '9rZW5CFQoTX3Bpbl9wcm90ZWN0aW9uX2tleUIICgZfZW1haWwaywEKIFJlZ2lzdGVyUGFzc3dv' + 'cmRsZXNzTm90aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW' + '9uSWQSLgoTZG93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SGwoJ' + 'bGFuZ19jb2RlGAMgASgJUghsYW5nQ29kZRIiCgpnb29nbGVfZmNtGAQgASgJSABSCWdvb2dsZU' + 'ZjbYgBAUINCgtfZ29vZ2xlX2ZjbRq8AQogQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRp' + 'b24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb25JZBIuChNkb3dubG9hZF' + '9hdXRoX3Rva2VuGAIgASgMUhFkb3dubG9hZEF1dGhUb2tlbhI/ChxhbHJlYWR5X3JlY2VpdmVk' + 'X21lc3NhZ2VfaWRzGAMgAygDUhlhbHJlYWR5UmVjZWl2ZWRNZXNzYWdlSWRzQgsKCUhhbmRzaG' + 'FrZQ=='); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { @@ -786,7 +786,6 @@ const ApplicationData$json = { ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, ApplicationData_GetAvailablePlans$json, - ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetCurrentPlanInfos$json, ApplicationData_RemoveAdditionalUser$json, ApplicationData_GetPrekeysByUserId$json, @@ -871,11 +870,6 @@ const ApplicationData_GetAvailablePlans$json = { '1': 'GetAvailablePlans', }; -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_GetAddAccountsInvites$json = { - '1': 'GetAddAccountsInvites', -}; - @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_GetCurrentPlanInfos$json = { '1': 'GetCurrentPlanInfos', @@ -1130,30 +1124,30 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEh' 'oKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpn' 'b29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIA' - 'EoA1IGdXNlcklkGhMKEUdldEF2YWlsYWJsZVBsYW5zGhcKFUdldEFkZEFjY291bnRzSW52aXRl' - 'cxoVChNHZXRDdXJyZW50UGxhbkluZm9zGi8KFFJlbW92ZUFkZGl0aW9uYWxVc2VyEhcKB3VzZX' - 'JfaWQYASABKANSBnVzZXJJZBotChJHZXRQcmVrZXlzQnlVc2VySWQSFwoHdXNlcl9pZBgBIAEo' - 'A1IGdXNlcklkGjIKF0dldFNpZ25lZFByZUtleUJ5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBn' - 'VzZXJJZBqbAQoSVXBkYXRlU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKANS' - 'DnNpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2Ch' - 'dzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGjUK' - 'DERvd25sb2FkRG9uZRIlCg5kb3dubG9hZF90b2tlbhgBIAEoDFINZG93bmxvYWRUb2tlbhpOCg' - 'pSZXBvcnRVc2VyEigKEHJlcG9ydGVkX3VzZXJfaWQYASABKANSDnJlcG9ydGVkVXNlcklkEhYK' - 'BnJlYXNvbhgCIAEoCVIGcmVhc29uGnEKC0lQQVB1cmNoYXNlEh0KCnByb2R1Y3RfaWQYASABKA' - 'lSCXByb2R1Y3RJZBIWCgZzb3VyY2UYAiABKAlSBnNvdXJjZRIrChF2ZXJpZmljYXRpb25fZGF0' - 'YRgDIAEoCVIQdmVyaWZpY2F0aW9uRGF0YRoPCg1JUEFGb3JjZUNoZWNrGg8KDURlbGV0ZUFjY2' - '91bnQaLAoRQWRkQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGjAKDVNl' - 'dExvZ2luVG9rZW4SHwoLbG9naW5fdG9rZW4YASABKAxSCmxvZ2luVG9rZW4aDAoKRGVwcmVjYX' - 'RlZBqOAQocUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeRIuChJlbmNyeXB0ZWRTZXJ2ZXJL' - 'ZXkYASABKAxSEmVuY3J5cHRlZFNlcnZlcktleRIrCg5waW5VbmxvY2tUb2tlbhgCIAEoDEgAUg' - '5waW5VbmxvY2tUb2tlbogBAUIRCg9fcGluVW5sb2NrVG9rZW4acAoYUGFzc3dvcmRsZXNzTm90' - 'aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSKwoRZW' - '5jcnlwdGVkX21lc3NhZ2UYAiABKAxSEGVuY3J5cHRlZE1lc3NhZ2UaUAoVUmVxdWVzdE1lbW9y' - 'aWVzVXBsb2FkEhIKBHNpemUYASABKANSBHNpemUSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3' - 'JpZ2luYWxEYXRlGjIKFUNvbmZpcm1NZW1vcmllc1VwbG9hZBIZCghtZWRpYV9pZBgBIAEoCVIH' - 'bWVkaWFJZBpICg9HZXRNZW1vcmllc0xpc3QSHwoLb2Zmc2V0X2RhdGUYASABKANSCm9mZnNldE' - 'RhdGUSFAoFbGltaXQYAiABKANSBWxpbWl0GisKDkdldE1lbW9yaWVzVXJsEhkKCG1lZGlhX2lk' - 'GAEgASgJUgdtZWRpYUlkGhIKEEdldE1lbW9yaWVzVXNhZ2VCEQoPQXBwbGljYXRpb25EYXRh'); + 'EoA1IGdXNlcklkGhMKEUdldEF2YWlsYWJsZVBsYW5zGhUKE0dldEN1cnJlbnRQbGFuSW5mb3Ma' + 'LwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldF' + 'ByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJl' + 'S2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcm' + 'VLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVk' + 'X3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGA' + 'MgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2Fk' + 'X3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdX' + 'Nlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoL' + 'SVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIA' + 'EoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRh' + 'Gg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlch' + 'IXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tl' + 'bhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkGo4BChxSZWdpc3RlclBhc3N3b3JkTG' + 'Vzc1JlY292ZXJ5Ei4KEmVuY3J5cHRlZFNlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVy' + 'S2V5EisKDnBpblVubG9ja1Rva2VuGAIgASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW' + '5VbmxvY2tUb2tlbhpwChhQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9u' + 'X2lkGAEgASgJUg5ub3RpZmljYXRpb25JZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW' + '5jcnlwdGVkTWVzc2FnZRpQChVSZXF1ZXN0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IE' + 'c2l6ZRIjCg1vcmlnaW5hbF9kYXRlGAIgASgDUgxvcmlnaW5hbERhdGUaMgoVQ29uZmlybU1lbW' + '9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dldE1lbW9yaWVzTGlz' + 'dBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdBgCIAEoA1IFbGltaX' + 'QaKwoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQaEgoQR2V0TWVt' + 'b3JpZXNVc2FnZUIRCg9BcHBsaWNhdGlvbkRhdGE='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart b/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart index ef98afd9..64551727 100644 --- a/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart +++ b/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart @@ -431,16 +431,14 @@ class RecoveryData extends $pb.GeneratedMessage { class SharedSecretData extends $pb.GeneratedMessage { factory SharedSecretData({ $core.List<$core.int>? recoveryData, - $core.List<$core.int>? encryptedServerKeyNonce, - $core.List<$core.int>? pinSeed, + $core.List<$core.int>? serverKeyProtection, $core.List<$core.int>? pinUnlockToken, $core.String? emailHint, }) { final result = create(); if (recoveryData != null) result.recoveryData = recoveryData; - if (encryptedServerKeyNonce != null) - result.encryptedServerKeyNonce = encryptedServerKeyNonce; - if (pinSeed != null) result.pinSeed = pinSeed; + if (serverKeyProtection != null) + result.serverKeyProtection = serverKeyProtection; if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken; if (emailHint != null) result.emailHint = emailHint; return result; @@ -463,9 +461,7 @@ class SharedSecretData extends $pb.GeneratedMessage { ..a<$core.List<$core.int>>( 1, _omitFieldNames ? '' : 'recoveryData', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'encryptedServerKeyNonce', $pb.PbFieldType.OY) - ..a<$core.List<$core.int>>( - 4, _omitFieldNames ? '' : 'pinSeed', $pb.PbFieldType.OY) + 3, _omitFieldNames ? '' : 'serverKeyProtection', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( 5, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY) ..aOS(6, _omitFieldNames ? '' : 'emailHint') @@ -501,39 +497,29 @@ class SharedSecretData extends $pb.GeneratedMessage { void clearRecoveryData() => $_clearField(1); @$pb.TagNumber(3) - $core.List<$core.int> get encryptedServerKeyNonce => $_getN(1); + $core.List<$core.int> get serverKeyProtection => $_getN(1); @$pb.TagNumber(3) - set encryptedServerKeyNonce($core.List<$core.int> value) => - $_setBytes(1, value); + set serverKeyProtection($core.List<$core.int> value) => $_setBytes(1, value); @$pb.TagNumber(3) - $core.bool hasEncryptedServerKeyNonce() => $_has(1); + $core.bool hasServerKeyProtection() => $_has(1); @$pb.TagNumber(3) - void clearEncryptedServerKeyNonce() => $_clearField(3); - - @$pb.TagNumber(4) - $core.List<$core.int> get pinSeed => $_getN(2); - @$pb.TagNumber(4) - set pinSeed($core.List<$core.int> value) => $_setBytes(2, value); - @$pb.TagNumber(4) - $core.bool hasPinSeed() => $_has(2); - @$pb.TagNumber(4) - void clearPinSeed() => $_clearField(4); + void clearServerKeyProtection() => $_clearField(3); @$pb.TagNumber(5) - $core.List<$core.int> get pinUnlockToken => $_getN(3); + $core.List<$core.int> get pinUnlockToken => $_getN(2); @$pb.TagNumber(5) - set pinUnlockToken($core.List<$core.int> value) => $_setBytes(3, value); + set pinUnlockToken($core.List<$core.int> value) => $_setBytes(2, value); @$pb.TagNumber(5) - $core.bool hasPinUnlockToken() => $_has(3); + $core.bool hasPinUnlockToken() => $_has(2); @$pb.TagNumber(5) void clearPinUnlockToken() => $_clearField(5); @$pb.TagNumber(6) - $core.String get emailHint => $_getSZ(4); + $core.String get emailHint => $_getSZ(3); @$pb.TagNumber(6) - set emailHint($core.String value) => $_setString(4, value); + set emailHint($core.String value) => $_setString(3, value); @$pb.TagNumber(6) - $core.bool hasEmailHint() => $_has(4); + $core.bool hasEmailHint() => $_has(3); @$pb.TagNumber(6) void clearEmailHint() => $_clearField(6); } diff --git a/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart b/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart index 0a12fc1f..2df56b6b 100644 --- a/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart @@ -116,21 +116,12 @@ const SharedSecretData$json = { '2': [ {'1': 'recovery_data', '3': 1, '4': 1, '5': 12, '10': 'recoveryData'}, { - '1': 'encrypted_server_key_nonce', + '1': 'server_key_protection', '3': 3, '4': 1, '5': 12, '9': 0, - '10': 'encryptedServerKeyNonce', - '17': true - }, - { - '1': 'pin_seed', - '3': 4, - '4': 1, - '5': 12, - '9': 1, - '10': 'pinSeed', + '10': 'serverKeyProtection', '17': true }, { @@ -138,7 +129,7 @@ const SharedSecretData$json = { '3': 5, '4': 1, '5': 12, - '9': 2, + '9': 1, '10': 'pinUnlockToken', '17': true }, @@ -147,14 +138,13 @@ const SharedSecretData$json = { '3': 6, '4': 1, '5': 9, - '9': 3, + '9': 2, '10': 'emailHint', '17': true }, ], '8': [ - {'1': '_encrypted_server_key_nonce'}, - {'1': '_pin_seed'}, + {'1': '_server_key_protection'}, {'1': '_pin_unlock_token'}, {'1': '_email_hint'}, ], @@ -163,8 +153,7 @@ const SharedSecretData$json = { /// Descriptor for `SharedSecretData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List sharedSecretDataDescriptor = $convert.base64Decode( 'ChBTaGFyZWRTZWNyZXREYXRhEiMKDXJlY292ZXJ5X2RhdGEYASABKAxSDHJlY292ZXJ5RGF0YR' - 'JAChplbmNyeXB0ZWRfc2VydmVyX2tleV9ub25jZRgDIAEoDEgAUhdlbmNyeXB0ZWRTZXJ2ZXJL' - 'ZXlOb25jZYgBARIeCghwaW5fc2VlZBgEIAEoDEgBUgdwaW5TZWVkiAEBEi0KEHBpbl91bmxvY2' - 'tfdG9rZW4YBSABKAxIAlIOcGluVW5sb2NrVG9rZW6IAQESIgoKZW1haWxfaGludBgGIAEoCUgD' - 'UgllbWFpbEhpbnSIAQFCHQobX2VuY3J5cHRlZF9zZXJ2ZXJfa2V5X25vbmNlQgsKCV9waW5fc2' - 'VlZEITChFfcGluX3VubG9ja190b2tlbkINCgtfZW1haWxfaGludA=='); + 'I3ChVzZXJ2ZXJfa2V5X3Byb3RlY3Rpb24YAyABKAxIAFITc2VydmVyS2V5UHJvdGVjdGlvbogB' + 'ARItChBwaW5fdW5sb2NrX3Rva2VuGAUgASgMSAFSDnBpblVubG9ja1Rva2VuiAEBEiIKCmVtYW' + 'lsX2hpbnQYBiABKAlIAlIJZW1haWxIaW50iAEBQhgKFl9zZXJ2ZXJfa2V5X3Byb3RlY3Rpb25C' + 'EwoRX3Bpbl91bmxvY2tfdG9rZW5CDQoLX2VtYWlsX2hpbnQ='); diff --git a/lib/src/model/protobuf/client/passwordless_recovery.proto b/lib/src/model/protobuf/client/passwordless_recovery.proto index 39243e19..ae295341 100644 --- a/lib/src/model/protobuf/client/passwordless_recovery.proto +++ b/lib/src/model/protobuf/client/passwordless_recovery.proto @@ -58,12 +58,9 @@ message SharedSecretData { // The recovery data is encrypted in case a second factor was chosen. bytes recovery_data = 1; - optional bytes encrypted_server_key_nonce = 3; + optional bytes server_key_protection = 3; - optional bytes pin_seed = 4; optional bytes pin_unlock_token = 5; optional string email_hint = 6; - } - diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index b173766f..f31cf0f2 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -778,15 +778,15 @@ class ApiService { Future getServerKeyForPasswordlessRecovery({ required int userId, - List? encryptedServerKeyNone, + List? serverKeyProtection, List? pinUnlockToken, List? pinProtectionKey, String? email, }) async { final get = Handshake_GetServerKeyForPasswordLessRecovery() ..userId = Int64(userId); - if (encryptedServerKeyNone != null) { - get.encryptedServerKeyNone = encryptedServerKeyNone; + if (serverKeyProtection != null) { + get.serverKeyProtection = serverKeyProtection; } if (pinUnlockToken != null) { get.pinUnlockToken = pinUnlockToken; @@ -803,7 +803,6 @@ class ApiService { return sendRequestSync(req, authenticated: false); } - Future submitRecoveryShare({ required String notificationId, required List encryptedMessage, @@ -954,7 +953,8 @@ class ApiService { /// Polls the server for new passwordless recovery notification messages. /// [alreadyReceivedIds] prevents the server from sending duplicates. - Future checkForPasswordlessNotification({ + Future + checkForPasswordlessNotification({ required String notificationId, required List downloadAuthToken, List? alreadyReceivedIds, diff --git a/lib/src/services/migrations.service.dart b/lib/src/services/migrations.service.dart index 4be4bfe1..f9cde856 100644 --- a/lib/src/services/migrations.service.dart +++ b/lib/src/services/migrations.service.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/signal_identity.model.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -170,9 +171,14 @@ Future runMigrations() async { }); } + if (userService.currentUser.appVersion < 118) { + await PasswordlessRecoveryService.migratePasswordlessRecovery(); + await UserService.update((u) => u.appVersion = 118); + } + if (kDebugMode) { assert( - AppState.latestAppVersionId == 117, + AppState.latestAppVersionId == 118, 'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.', ); assert( diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index ecca9060..0ac3d199 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -5,7 +5,7 @@ import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart' hide Hmac; import 'package:cryptography_plus/cryptography_plus.dart' - show Hmac, Mac, SecretBox, SecretKey, Xchacha20; + show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/core/bridge/wrapper.dart'; @@ -24,7 +24,7 @@ import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/avatars.dart' show getAvatarSvg; + import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -105,9 +105,7 @@ class PasswordlessRecoveryService { final shareUser = TrustedFriendShare_User( userId: Int64(contact.userId), displayName: getContactDisplayName(contact), - avatar: contact.avatarSvgCompressed != null - ? utf8.encode(getAvatarSvg(contact.avatarSvgCompressed!)) - : null, + avatar: contact.avatarSvgCompressed, ); final trustedFriendShare = TrustedFriendShare( @@ -187,20 +185,24 @@ class PasswordlessRecoveryService { switch (secondFactorType) { case SecondFactorType.email: - config.email = secondFactorValue; - emailHint = createEmailHint(secondFactorValue); + config.email = secondFactorValue.toLowerCase(); + emailHint = createEmailHint(config.email!); // E-Mail Protection: // - Server can only learn the email during recovery. Ensured as the server gets the NONCE to decrypt only during recovery. // - Trusted-friends: Server key is only sent to the mail, they would need access to the user's mail account. - secondFactorEncryptedServerKeyKey = SecretKey( - Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes), + config.serverKeyProtection = getRandomUint8List(32); + + final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32); + secondFactorEncryptedServerKeyKey = await hkdf.deriveKey( + secretKey: SecretKey(config.serverKeyProtection!), + nonce: utf8.encode(config.email!), ); case SecondFactorType.pin: // The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin - config.pinSeed = getRandomUint8List(32); + config.serverKeyProtection = getRandomUint8List(32); // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the // unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion. @@ -210,16 +212,12 @@ class PasswordlessRecoveryService { // - Server: Does not know the seed. // - Trusted friends: Can only check the result 10 times before the server deletes the key. As they do not have // the mac and the cipher text they are unable to brute-force the pin locally. And the server only allows 10 tries. - final pinProtectionKey = await Hmac.sha256().calculateMac( - Uint8List.fromList(utf8.encode(secondFactorValue)), - secretKey: SecretKey(config.pinSeed!), + final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32); + secondFactorEncryptedServerKeyKey = await hkdf.deriveKey( + secretKey: SecretKey(config.serverKeyProtection!), + nonce: utf8.encode(secondFactorValue), ); - // To restore the user has to provide the server with this encryption key. The server then can verify the - // correct pin was entered, when decrypting the server key as he also receives the mac and nonce from the user - // Only when the mac is correct the server provides the user with the serverKey. - secondFactorEncryptedServerKeyKey = SecretKey(pinProtectionKey.bytes); - case SecondFactorType.none: } @@ -234,18 +232,16 @@ class PasswordlessRecoveryService { nonce: xchacha20.newNonce(), ); - // The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit - // because of XChaCha), he is unable to decrypt the server key without the help of the trusted friends. This - // ensures that the server never learns the user's original pin, as he is missing the pin_seed and also unable to - // brute-force the email of the user as he does not have the nonce. + // The server only gets the encrypted server key, the mac, and the nonce. + // This ensures that the server never learns the user's original pin, as he is missing the serverKeyProtection and also unable to + // brute-force the email of the user. encryptedServerKey = Uint8List.fromList([ ...secretBox.cipherText, ...secretBox.mac.bytes, + ...secretBox.nonce, ]); - config - ..encryptedServerKeyNonce = secretBox.nonce - ..encryptedServerKey = encryptedServerKey; + config.encryptedServerKey = encryptedServerKey; } // 3. Using shamir's secret to generate the shares for the users. @@ -275,10 +271,9 @@ class PasswordlessRecoveryService { final sharedSecretData = SharedSecretData( recoveryData: recoveryData, - pinSeed: config.pinSeed, + serverKeyProtection: config.serverKeyProtection, pinUnlockToken: config.pinUnlockToken, emailHint: emailHint, - encryptedServerKeyNonce: config.encryptedServerKeyNonce, ).writeToBuffer(); // 3.2. Use the amount of trusted friends to generate the shares @@ -322,29 +317,31 @@ class PasswordlessRecoveryService { static Future testPin(String pin) async { final config = userService.currentUser.passwordLessRecovery; - if (config?.pinSeed == null || config?.encryptedServerKey == null) { + if (config?.serverKeyProtection == null || + config?.encryptedServerKey == null) { return false; } try { - final pinProtectionKey = await Hmac.sha256().calculateMac( - Uint8List.fromList(utf8.encode(pin)), - secretKey: SecretKey(config!.pinSeed!), - ); - - final secondFactorEncryptedServerKeyKey = SecretKey( - pinProtectionKey.bytes, + final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32); + final secondFactorEncryptedServerKeyKey = await hkdf.deriveKey( + secretKey: SecretKey(config!.serverKeyProtection!), + nonce: utf8.encode(pin), ); final xchacha20 = Xchacha20.poly1305Aead(); final combined = config.encryptedServerKey!; - final cipherText = combined.sublist(0, combined.length - 16); - final macBytes = combined.sublist(combined.length - 16); + final nonceBytes = combined.sublist(combined.length - 24); + final macBytes = combined.sublist( + combined.length - 40, + combined.length - 24, + ); + final cipherText = combined.sublist(0, combined.length - 40); final secretBox = SecretBox( cipherText, - nonce: config.encryptedServerKeyNonce!, + nonce: nonceBytes, mac: Mac(macBytes), ); @@ -630,4 +627,53 @@ class PasswordlessRecoveryService { return didUpdate; } + + static Future migratePasswordlessRecovery() async { + final config = userService.currentUser.passwordLessRecovery; + if (config == null) return; + + final oldTrustedFriends = await (twonlyDB.select( + twonlyDB.contacts, + )..where((t) => t.recoveryIsTrustedFriend.equals(true))).get(); + final trustedFriendIds = oldTrustedFriends.map((e) => e.userId).toList(); + + if (trustedFriendIds.isEmpty) return; + + if (config.email != null) { + await enablePasswordlessRecovery( + trustedFriendIds: trustedFriendIds, + secondFactorType: SecondFactorType.email, + secondFactorValue: config.email!, + threshold: config.threshold, + ); + } else if (config.pinUnlockToken == null) { + await enablePasswordlessRecovery( + trustedFriendIds: trustedFriendIds, + secondFactorType: SecondFactorType.none, + secondFactorValue: '', + threshold: config.threshold, + ); + } else { + // It's PIN, we can't migrate it because we don't have the PIN. We delete it so the user has to do it again. + for (final contact in oldTrustedFriends) { + try { + await sendCipherText( + contact.userId, + pb.EncryptedContent( + passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery( + delete: true, + ), + ), + ); + } catch (e) { + Log.error( + 'Failed to send delete PasswordLessRecovery message to contact ${contact.userId}: $e', + ); + } + } + + await twonlyDB.contactsDao.resetRecoveryDataForAllContacts(); + await UserService.update((u) => u.passwordLessRecovery = null); + } + } } diff --git a/lib/src/utils/avatars.dart b/lib/src/utils/avatars.dart index 255a7253..8bfe349d 100644 --- a/lib/src/utils/avatars.dart +++ b/lib/src/utils/avatars.dart @@ -9,8 +9,51 @@ import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/log.dart'; +const defaultAvatarSvg = ''' + + + + + + + + + + + + + + + + + + + + + + + +'''; + String getAvatarSvg(Uint8List avatarSvgCompressed) { - return utf8.decode(gzip.decode(avatarSvgCompressed)); + try { + // Check for GZIP magic bytes (0x1F 0x8B) + final isGzip = + avatarSvgCompressed.length >= 2 && + avatarSvgCompressed[0] == 0x1F && + avatarSvgCompressed[1] == 0x8B; + + final decodedBytes = isGzip + ? gzip.decode(avatarSvgCompressed) + : avatarSvgCompressed; + return utf8.decode(decodedBytes); + } catch (e) { + Log.error('Failed to decode avatar SVG: $e'); + return defaultAvatarSvg; + } } Future createPushAvatars({int? forceForUserId}) async { diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart index 045d54b6..bf5d3170 100644 --- a/lib/src/visual/views/onboarding/recover_passwordless.view.dart +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:cryptography_plus/cryptography_plus.dart' - show Hmac, Mac, SecretBox, SecretKey, Xchacha20; + show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -21,6 +21,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart'; +import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -239,7 +240,8 @@ class _RecoverPasswordlessState extends State { final userId = shares.first.myUserId; Uint8List? serverKey; - if (reconstructed.hasPinSeed()) { + if (!reconstructed.hasEmailHint() && + reconstructed.hasServerKeyProtection()) { final pin = _secondFactorController.text.trim(); if (pin.isEmpty) { showSnackbar( @@ -252,18 +254,18 @@ class _RecoverPasswordlessState extends State { return; } - // Calculate pinProtectionKey - final pinProtectionKey = await Hmac.sha256().calculateMac( - Uint8List.fromList(utf8.encode(pin)), - secretKey: SecretKey(reconstructed.pinSeed), + // Calculate pinProtectionKey via Hkdf + final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32); + final pinKey = await hkdf.deriveKey( + secretKey: SecretKey(reconstructed.serverKeyProtection), + nonce: utf8.encode(pin), ); // Fetch serverKey final res = await apiService.getServerKeyForPasswordlessRecovery( userId: userId, pinUnlockToken: reconstructed.pinUnlockToken, - pinProtectionKey: pinProtectionKey.bytes, - encryptedServerKeyNone: reconstructed.encryptedServerKeyNonce, + pinProtectionKey: await pinKey.extractBytes(), ); if (res.isError) { @@ -303,7 +305,7 @@ class _RecoverPasswordlessState extends State { final res = await apiService.getServerKeyForPasswordlessRecovery( userId: userId, email: email, - encryptedServerKeyNone: reconstructed.encryptedServerKeyNonce, + serverKeyProtection: reconstructed.serverKeyProtection, ); if (res.isError) { @@ -490,7 +492,9 @@ class _RecoverPasswordlessState extends State { child: Column( children: [ AvatarIcon( - svg: utf8.decode(first.myAvatarSvg ?? []), + svg: first.myAvatarSvg != null + ? getAvatarSvg(Uint8List.fromList(first.myAvatarSvg!)) + : null, fontSize: 60, ), const SizedBox(height: 12), @@ -568,7 +572,8 @@ class _RecoverPasswordlessState extends State { ), ) else if (_reconstructedSecret != null) ...[ - if (_reconstructedSecret!.hasPinSeed()) ...[ + if (_reconstructedSecret!.hasServerKeyProtection() && + !_reconstructedSecret!.hasEmailHint()) ...[ MyInput( controller: _secondFactorController, hintText: context.lang.passwordlessRecoveryMethodPinHint, diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart index 9afe306a..5dae377d 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/settings.passwordless_recovery.view.dart @@ -116,7 +116,7 @@ class PasswordLessRecoverySettings extends StatelessWidget { secondFactorLabel = context.lang .passwordlessRecoverySecondFactorEmailLabel(config.email!); secondFactorIcon = Icons.email_outlined; - } else if (config.pinSeed != null) { + } else if (config.serverKeyProtection != null && config.email == null) { secondFactorLabel = context.lang.passwordlessRecoverySecondFactorPin; secondFactorIcon = Icons.pin_outlined; actionButton = MyButton( diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart index f1bdcd9e..22156a8f 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart @@ -69,7 +69,7 @@ class _PasswordLessRecoverySetupState extends State { _secondFactor = SecondFactorType.email; _emailController.text = config.email!; _threshold = 2; - } else if (config.pinSeed != null) { + } else if (config.serverKeyProtection != null && config.email == null) { _secondFactor = SecondFactorType.pin; _threshold = 2; } else { diff --git a/proto.md b/proto.md new file mode 100644 index 00000000..d8ac8c97 --- /dev/null +++ b/proto.md @@ -0,0 +1,170 @@ +--- +title: "Break my Passwordless Recovery Protocol and get a 50€ Bug-Bounty" +date: 2026-06-23 +description: I designed a passwordless recovery protocol using trusted friends and a server-side second factor for my privacy-focused messaging app twonly. To ensure its security before release, I am launching a 50€ bug bounty challenge for anyone who can find a vulnerability in the protocol. +keywords: twonly, Bug-Bounty, Protocol-Design +--- +# Break my Passwordless Recovery Protocol and get a 50€ Bug-Bounty + +A year ago I started building an Open-Source Snapchat alternative called [twonly](https://twonly.eu), as I liked the basic features of Snapchat, but wanted my images and text messages to not be [scanned](https://heise.de/-11246086), viewed by their [employees](https://www.vice.com/en/article/snapchat-employees-abused-data-access-spy-on-users-snaplion/), or potentially by attackers or [a government](https://www.bbc.com/news/world-europe-68099669). + +Directly from the start, users frequently lost access to their account after installing the app on a new device or losing their old phone. So a recovery mechanism was needed. Signal and WhatsApp use, beside their ([unencrypted (WhatsApp)](https://wire.com/en/blog/when-opt-in-security-fails-whatsapp-backup-example)) backup, the user's phone number to identify the user and allow them to reset their private key. + +But I do not want to use phone numbers, as this would undermine the [users' privacy](https://twonly.eu/en/blog/2026-mutual-friends.html#the-problem-with-phone-numbers). Also, allowing users to recover their accounts using solely a phone number moves the Root of Trust from the private key to the phone number and with that to the server, which claims to have verified it. The server can then notify contacts about the user's new public key, which the clients accept. Yes, the clients show "Your safety numbers have changed", but this is ignored by almost all users (from my own experience). + +As a starting point I decided to use a similar approach to [Threema's Safe](https://threema.com/en/faq/threema-safe-security), as the protocol was already designed and audited by experts. But this system requires the user to remember their password to protect the uploaded private key. And believe it or not, users frequently forget their password, again requiring them to recreate their account, losing all their friends, rejoining existing groups, and, in the case of an E2EE encrypted image backup which I am planning to implement, losing access to their precious images. To fix this I started designing a passwordless recovery protocol involving Trusted Friends and the server as a second factor. + +When involving friends to store a share of the user's secret key, it must be ensured that these are actually their friends' accounts and not an attacker impersonating their actual friends or a potentially compromised server performing an active MITM attack. For this, I already [rolled](https://twonly.eu/en/blog/2026-mutual-friends.html) out as part of [my master's thesis](https://mastodon.social/@twonly/115966775370449880) an improved Authentication Ceremony which users can more easily understand and which is partially automated via a Web of Trust approach. Users then can only select such verified friends. + +But as I want this protocol to be secure, it should ideally be verified by external security professionals. However, as a student, I cannot afford a professional audit. So before releasing it to the public, I am starting a 50€ Bug Bounty challenge with this blog post. The deal: **You find a flaw in my protocol, you receive 50€.** And yes, this may not sound like much, but hey, I am a student and have to pay this out of my own pocket. At the end of this post, you will find the rules and the security goals for the bug bounty. + +## The Passwordless Recovery Protocol + +

+Update (July 19, 2026): I received feedback from a cryptography expert, who pointed out that my original trick of withholding the nonce from the server might work in practice, but it prevents a formal security proof under standard AEAD models. So I updated the protocol: Instead of the nonce, the encryption key for the serverKey is now withheld, distributed to the trusted friends, and only sent to the server during the recovery. +

+ +To enable the passwordless recovery, a user first has to select at least **T + 2** verified trusted friends (N), where T is the threshold for [Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing). The + 2 is there so that even if a couple of friends lose their phone or become unreachable, there are still enough shares left to recover. Then they can decide to enable a second factor, either a PIN or an email. From there, the user has finished their task. In the background the user's secret key is then split into multiple parts and then distributed to the selected friends. In case the second factor was enabled, these shares are further encrypted using a server key. Here is the full setup flow: + +```mermaid +sequenceDiagram + participant U as User + participant S as Server + participant F as Friends + + U->>U: RecoveryData = userId + secret key + + alt Second Factor: Email + U->>U: Generate random
serverKey (32 bytes)
serverKeyProtection (32 bytes)
Nonce (24 bytes) + U->>U: emailKey = HKDF(ikm=serverKeyProtection, salt=email) + U->>U: protectedServerKey, MAC = XChaCha20-Poly1305.encrypt(serverKey, emailKey, Nonce) + U->>S: protectedServerKey + MAC + Nonce + U->>U: RecoveryData = XChaCha20-Poly1305.encrypt(RecoveryData, serverKey) + else Second Factor: PIN + U->>U: Generate random
serverKey (32 bytes)
serverKeyProtection (32 bytes)
pinUnlockToken (32 bytes)
Nonce (24 bytes) + U->>U: pinKey = HKDF(ikm=serverKeyProtection, salt=pin) + U->>U: protectedServerKey, MAC = XChaCha20-Poly1305.encrypt(serverKey, pinKey, Nonce) + U->>S: protectedServerKey + MAC + pinUnlockToken + Nonce + U->>U: RecoveryData = XChaCha20-Poly1305.encrypt(RecoveryData, serverKey) + else No Second Factor + U->>U: increase threshold T from >=2 to >=4 + end + + U->>U: Bundle SharedSecretData:
RecoveryData +
(optional: serverKeyProtection + pinUnlockToken) + + U->>U: Shamir split SharedSecretData
into N shares (threshold T) + + U->>F: Send each share via
E2E encrypted message + F->>F: Store share locally +``` + +Because the server does not receive the `serverKeyProtection` (32 bytes), the server is unable to brute-force the user's email or PIN. But because it has the MAC, it can then during recovery ensure that it received the original user's input. + + +### Recovery Flow + +When a user now forgets their backup password and has enabled the passwordless recovery, they can ask their friends to send them their shares. For this, the following flow is used to make it as easy as possible for all participants: + + +```mermaid +sequenceDiagram + participant R as Recovering User + participant S as Server + participant F as Trusted Friend + participant APP as Friend's App + + + R->>R: generates random
notificationID (UUID)
downloadAuthToken (32 bytes)
notificationKey (32 bytes) + + R->>R: Read out pushToken + + R->>S: notificationID, downloadAuthToken,
pushToken + + R->>R: Creates link (behind fragment):
notificationID, notificationKey + + R->>F: Shows QR code or shares link + + F->>APP: Opens recovery link / scans QR + APP->>APP: Parse notificationId +
notificationKey from URL + + APP->>F: "Select which contact is recovering" + F->>APP: Selects contact from list + APP->>F: "Are you sure?" phishing-prevention dialog
with a 10s timer + F->>APP: Confirms + + APP->>APP: Encrypt stored share + userId
with notificationKey
(XChaCha20-Poly1305) + APP->>S: Submit encrypted envelope
(notificationId) + S->>R: Push notification:
"A friend submitted a share" +``` + +A symmetric encryption key was deliberately chosen over an asymmetric keypair: the goal is to protect submitted shares from the server, not from the user. Only the user holding the downloadAuthToken can download the submissions, and only the one holding the notificationKey can decrypt them. This makes the protocol post-quantum safe from the start, without requiring large public keys which cannot be shared via a QR code or a link. + +To reduce potential phishing attacks, the user must manually select which friend they want to help, and then confirm their selection. This confirmation has a 10s timer and a red warning, asking them if they are sure that they have been asked by the selected user via a secure channel (like in-person or via Signal). + +In the last step, the recovering user now can use the collected shares to recover their secret key: + + +```mermaid +sequenceDiagram + participant R as Recovering User + participant S as Server + participant I as User's inbox + + R->>S: Download encrypted shares
(authenticated via downloadAuthToken) + R->>R: Decrypt userId + shares
with notificationKey + R->>R: Feed T shares into
Shamir reconstruction + R->>R: Obtain SharedSecretData + + alt Email second factor + R->>R: Inputs email + R->>S: Send userId, SharedSecretData.serverKeyProtection, email + S->>S: emailKey = HKDF(ikm=serverKeyProtection, salt=email) + S->>S: serverKey = XChaCha20-Poly1305.decrypt(protectedServerKey, emailKey, stored Nonce, stored MAC) + S->>I: Send serverKey + I->>R: Copy serverKey + R->>R: Decrypt RecoveryData
with serverKey + else PIN second factor + R->>R: pinKey = HKDF(ikm=SharedSecretData.serverKeyProtection,
salt=pin) + R->>S: Send userId, pinKey + SharedSecretData.pinUnlockToken + S->>S: Check pinUnlockToken + S->>S: serverKey = XChaCha20-Poly1305.decrypt(protectedServerKey, pinKey, stored Nonce, stored MAC)
(~10 tries max) + S->>R: Return serverKey + R->>R: Decrypt RecoveryData
with serverKey + else No second factor + R->>R: RecoveryData is directly
in SharedSecretData + end + + R->>R: Restore userId + secret key +``` + +The server will automatically delete the `protectedServerKey` when more than 10 tries for the PIN were performed. The reason for the `pinUnlockToken` is to prevent denial-of-service: without it, anyone could just spam the recovery endpoint with wrong keys and trigger the 10-attempt lockout, permanently destroying the user's `protectedServerKey`. Since only the trusted friends hold the Shamir shares containing the `pinUnlockToken`, only they (in case they collude) or the legitimate recovering user can actually initiate a recovery attempt. + +As this protocol is designed to only recover small amounts of data (the secret key), this is combined with an encrypted backup in the cloud, which is protected by the user's secret key that can then be downloaded by the user, restoring their contacts, messages and images. + +## Rules + +- Below is a list of my security goals; compromise at least one and you qualify for the 50€ Bug-Bounty. If you find another security issue or encounter any other problem, I'm all ears. +- You can ask your agent, but you must disclose that, as I am curious what you prompted :). +- First come, first served. The bug-bounty is limited to one payout. Sorry, I am a student :/ +- I reserve the right to decide for myself if the flaw is worth the bug bounty payout. +- The Bug-Bounty is only paid out in Cash, Bank-Transfer, or WERO. + +### Security Goals + +- The server should not be able to link a user and an email until the user recovers their account. +- The server should not be able to get access to the plaintext PIN. +- The server should never be able to get access to the user's secret key. +- A single trusted friend should never be able to recover the user's secret key on their own. +- The trusted friends should, in case the second factor is enabled, not be able to collude and recover the user's secret key. + +### Assumptions + +- The server does not collude with the trusted friends. +- The trusted friends are unable to guess the user's PIN with under 10 attempts. +- The trusted friends are unable to access the user's inbox. +- In case of no second factor, the trusted friends (at least 4) do not collude. + + +## Contact + +If you have found something, please send either an [email](mailto:security@tsmr.eu) or find me on Signal: tobi.02. \ No newline at end of file diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index 37d09e63..c48c2b31 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -42,7 +42,7 @@ void main() { image: 'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg', vendor: Vendor.mastodonSocialMediaPosting, - shareAction: 80, + shareAction: 60, likeAction: 290, ), LinkParserTest( diff --git a/test/services/passwordless_recovery_service_test.dart b/test/services/passwordless_recovery_service_test.dart index 43d6d81c..91aaa851 100644 --- a/test/services/passwordless_recovery_service_test.dart +++ b/test/services/passwordless_recovery_service_test.dart @@ -502,6 +502,7 @@ void main() { final msg = pb.EncryptedContent_PasswordLessRecovery() ..delete = false + ..threshold = Int64(2) ..recoverySecretShare = [11, 22, 33]; await PasswordlessRecoveryService.handlePasswordlessRecovery( From 060f90ff6bfaaa4aefbd8638f7ce143c53fd62a3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 14:34:36 +0200 Subject: [PATCH 14/29] bump version for github --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b69372a2..afbcf15e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.4.0+156 +version: 0.4.1+158 environment: sdk: ^3.11.0 From 95054068f736ec01ff74b60567553fce0747ea82 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 15:37:22 +0200 Subject: [PATCH 15/29] update comments --- .../generated/passwordless_recovery.pb.dart | 95 +++---------------- .../passwordless_recovery.pbjson.dart | 14 --- .../client/passwordless_recovery.proto | 37 +++----- 3 files changed, 26 insertions(+), 120 deletions(-) diff --git a/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart b/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart index 64551727..8c5af53f 100644 --- a/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart +++ b/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart @@ -17,80 +17,8 @@ import 'package:protobuf/protobuf.dart' as $pb; export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; -/// Send from the person who tries to recover their account. -/// This can be done via a link, which will then be opend in the app of the contact. -/// The contact than has to manualy select from which user he got the request. -/// -> Using this phishing is harder, as the user has to manualy select the user to recovery -/// -> The user who wants to recover his account does not need to remember her old username -class RecoveryRequest extends $pb.GeneratedMessage { - factory RecoveryRequest({ - $core.String? notificationId, - $core.List<$core.int>? publicKey, - }) { - final result = create(); - if (notificationId != null) result.notificationId = notificationId; - if (publicKey != null) result.publicKey = publicKey; - return result; - } - - RecoveryRequest._(); - - factory RecoveryRequest.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory RecoveryRequest.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'RecoveryRequest', - package: const $pb.PackageName( - _omitMessageNames ? '' : 'passwordless_recovery'), - createEmptyInstance: create) - ..aOS(1, _omitFieldNames ? '' : 'notificationId') - ..a<$core.List<$core.int>>( - 2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - RecoveryRequest clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - RecoveryRequest copyWith(void Function(RecoveryRequest) updates) => - super.copyWith((message) => updates(message as RecoveryRequest)) - as RecoveryRequest; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static RecoveryRequest create() => RecoveryRequest._(); - @$core.override - RecoveryRequest createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static RecoveryRequest getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static RecoveryRequest? _defaultInstance; - - @$pb.TagNumber(1) - $core.String get notificationId => $_getSZ(0); - @$pb.TagNumber(1) - set notificationId($core.String value) => $_setString(0, value); - @$pb.TagNumber(1) - $core.bool hasNotificationId() => $_has(0); - @$pb.TagNumber(1) - void clearNotificationId() => $_clearField(1); - - @$pb.TagNumber(2) - $core.List<$core.int> get publicKey => $_getN(1); - @$pb.TagNumber(2) - set publicKey($core.List<$core.int> value) => $_setBytes(1, value); - @$pb.TagNumber(2) - $core.bool hasPublicKey() => $_has(1); - @$pb.TagNumber(2) - void clearPublicKey() => $_clearField(2); -} - -/// Used as envelope for TrustedFriendShare and RecoveryData +/// Used as envelope for TrustedFriendShare and RecoveryData. +/// Encrypted using XChaCha20-Poly1305. class EncryptedEnvelope extends $pb.GeneratedMessage { factory EncryptedEnvelope({ $core.List<$core.int>? encryptedData, @@ -254,8 +182,8 @@ class TrustedFriendShare_User extends $pb.GeneratedMessage { void clearAvatar() => $_clearField(3); } -/// Send from the trusted friend to -/// This is encrypted with the received public key. +/// Sent from the trusted friend to the recovering user via the server. +/// This is symmetrically encrypted using the encryption key received from the recovery link. class TrustedFriendShare extends $pb.GeneratedMessage { factory TrustedFriendShare({ TrustedFriendShare_User? trustedFriend, @@ -313,7 +241,7 @@ class TrustedFriendShare extends $pb.GeneratedMessage { $pb.GeneratedMessage.$_defaultFor(create); static TrustedFriendShare? _defaultInstance; - /// This allows to display the user which user has send him his recovery data. + /// This allows to display to the recovering user which trusted friend has sent their recovery data. @$pb.TagNumber(1) TrustedFriendShare_User get trustedFriend => $_getN(0); @$pb.TagNumber(1) @@ -325,7 +253,7 @@ class TrustedFriendShare extends $pb.GeneratedMessage { @$pb.TagNumber(1) TrustedFriendShare_User ensureTrustedFriend() => $_ensure(0); - /// This allows to display the userdata, showing that he is recovering the correct person. + /// This allows to display the userdata, showing that the trusted friend is recovering the correct person. @$pb.TagNumber(2) TrustedFriendShare_User get shareUser => $_getN(1); @$pb.TagNumber(2) @@ -337,7 +265,7 @@ class TrustedFriendShare extends $pb.GeneratedMessage { @$pb.TagNumber(2) TrustedFriendShare_User ensureShareUser() => $_ensure(1); - /// The minimum threshold required to decrypte the shares. + /// The minimum threshold required to reconstruct the shares. @$pb.TagNumber(3) $core.int get threshold => $_getIZ(2); @$pb.TagNumber(3) @@ -347,7 +275,7 @@ class TrustedFriendShare extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearThreshold() => $_clearField(3); - /// The actual share which will become: SharedSecretData + /// The actual share which will be used to reconstruct SharedSecretData. @$pb.TagNumber(4) $core.List<$core.int> get sharedSecretData => $_getN(3); @$pb.TagNumber(4) @@ -427,7 +355,7 @@ class RecoveryData extends $pb.GeneratedMessage { void clearKeyManager() => $_clearField(3); } -/// After received all shares this is decrypted by the user restoring its own +/// After receiving threshold shares, this is reconstructed by the recovering user. class SharedSecretData extends $pb.GeneratedMessage { factory SharedSecretData({ $core.List<$core.int>? recoveryData, @@ -486,7 +414,7 @@ class SharedSecretData extends $pb.GeneratedMessage { $pb.GeneratedMessage.$_defaultFor(create); static SharedSecretData? _defaultInstance; - /// The recovery data is encrypted in case a second factor was chosen. + /// The recovery data (RecoveryData). It is encrypted with the server_key in case a second factor was chosen. @$pb.TagNumber(1) $core.List<$core.int> get recoveryData => $_getN(0); @$pb.TagNumber(1) @@ -496,6 +424,8 @@ class SharedSecretData extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearRecoveryData() => $_clearField(1); + /// Combined with the user's PIN/Email via HKDF to derive the key to decrypt the encrypted server_key. + /// The server NEVER sees this value. @$pb.TagNumber(3) $core.List<$core.int> get serverKeyProtection => $_getN(1); @$pb.TagNumber(3) @@ -505,6 +435,7 @@ class SharedSecretData extends $pb.GeneratedMessage { @$pb.TagNumber(3) void clearServerKeyProtection() => $_clearField(3); + /// Used to authenticate with the server to fetch the encrypted server_key. @$pb.TagNumber(5) $core.List<$core.int> get pinUnlockToken => $_getN(2); @$pb.TagNumber(5) diff --git a/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart b/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart index 2df56b6b..f23f122c 100644 --- a/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart @@ -15,20 +15,6 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; -@$core.Deprecated('Use recoveryRequestDescriptor instead') -const RecoveryRequest$json = { - '1': 'RecoveryRequest', - '2': [ - {'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'}, - {'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'}, - ], -}; - -/// Descriptor for `RecoveryRequest`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List recoveryRequestDescriptor = $convert.base64Decode( - 'Cg9SZWNvdmVyeVJlcXVlc3QSJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb2' - '5JZBIdCgpwdWJsaWNfa2V5GAIgASgMUglwdWJsaWNLZXk='); - @$core.Deprecated('Use encryptedEnvelopeDescriptor instead') const EncryptedEnvelope$json = { '1': 'EncryptedEnvelope', diff --git a/lib/src/model/protobuf/client/passwordless_recovery.proto b/lib/src/model/protobuf/client/passwordless_recovery.proto index ae295341..90104184 100644 --- a/lib/src/model/protobuf/client/passwordless_recovery.proto +++ b/lib/src/model/protobuf/client/passwordless_recovery.proto @@ -1,42 +1,28 @@ syntax = "proto3"; package passwordless_recovery; -// Recovery Process -// - Generating: TempID and a new assymetric key pair -// - Uploading to the server TempID + Push Tokens so the server can notify the user that someone helped him - - -// Send from the person who tries to recover their account. -// This can be done via a link, which will then be opend in the app of the contact. -// The contact than has to manualy select from which user he got the request. -// -> Using this phishing is harder, as the user has to manualy select the user to recovery -// -> The user who wants to recover his account does not need to remember her old username -message RecoveryRequest { - string notification_id = 1; - bytes public_key = 2; -} - -// Used as envelope for TrustedFriendShare and RecoveryData +// Used as envelope for TrustedFriendShare and RecoveryData. +// Encrypted using XChaCha20-Poly1305. message EncryptedEnvelope { bytes encrypted_data = 1; bytes iv = 2; bytes mac = 3; } -// Send from the trusted friend to -// This is encrypted with the received public key. +// Sent from the trusted friend to the recovering user via the server. +// This is symmetrically encrypted using the encryption key received from the recovery link. message TrustedFriendShare { - // This allows to display the user which user has send him his recovery data. + // This allows to display to the recovering user which trusted friend has sent their recovery data. User trusted_friend = 1; - // This allows to display the userdata, showing that he is recovering the correct person. + // This allows to display the userdata, showing that the trusted friend is recovering the correct person. User share_user = 2; - // The minimum threshold required to decrypte the shares. + // The minimum threshold required to reconstruct the shares. int32 threshold = 3; - // The actual share which will become: SharedSecretData + // The actual share which will be used to reconstruct SharedSecretData. bytes shared_secret_data = 4; message User { @@ -52,14 +38,17 @@ message RecoveryData { bytes key_manager = 3; } -// After received all shares this is decrypted by the user restoring its own +// After receiving threshold shares, this is reconstructed by the recovering user. message SharedSecretData { - // The recovery data is encrypted in case a second factor was chosen. + // The recovery data (RecoveryData). It is encrypted with the server_key in case a second factor was chosen. bytes recovery_data = 1; + // Combined with the user's PIN/Email via HKDF to derive the key to decrypt the encrypted server_key. + // The server NEVER sees this value. optional bytes server_key_protection = 3; + // Used to authenticate with the server to fetch the encrypted server_key. optional bytes pin_unlock_token = 5; optional string email_hint = 6; From 3c26bf55a8da70fb37366563c06dcf4217b662a4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 15:49:06 +0200 Subject: [PATCH 16/29] move file --- lib/locator.dart | 2 +- lib/src/services/{ => api}/api.service.dart | 0 lib/src/services/memories/cloud.service.dart | 0 pubspec.lock | 7 +++++ pubspec.yaml | 3 ++ test/mocks/test_client.dart | 31 ++++++++++++++----- test/mocks/user_environment.dart | 2 +- test/services/api_and_c2c_test.dart | 2 +- test/services/backup_service_test.dart | 2 +- test/services/group_service_test.dart | 2 +- .../key_verification_service_test.dart | 2 +- .../passwordless_recovery_service_test.dart | 2 +- 12 files changed, 41 insertions(+), 14 deletions(-) rename lib/src/services/{ => api}/api.service.dart (100%) create mode 100644 lib/src/services/memories/cloud.service.dart diff --git a/lib/locator.dart b/lib/locator.dart index a9333733..8c6806bc 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,6 +1,6 @@ import 'package:get_it/get_it.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/user.service.dart'; final GetIt locator = GetIt.instance; diff --git a/lib/src/services/api.service.dart b/lib/src/services/api/api.service.dart similarity index 100% rename from lib/src/services/api.service.dart rename to lib/src/services/api/api.service.dart diff --git a/lib/src/services/memories/cloud.service.dart b/lib/src/services/memories/cloud.service.dart new file mode 100644 index 00000000..e69de29b diff --git a/pubspec.lock b/pubspec.lock index 7b57597c..adc20a54 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -613,6 +613,13 @@ packages: url: "https://github.com/yenchieh/flutter_android_volume_keydown.git" source: git version: "1.0.1" + flutter_blurhash: + dependency: "direct main" + description: + path: "dependencies/flutter_blurhash" + relative: true + source: path + version: "0.9.1" flutter_cache_manager: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index afbcf15e..21d16322 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: screen_protector: ^1.5.1 flutter_markdown_plus: ^1.0.7 exif: ^3.3.0 + flutter_blurhash: ^0.9.1 # With high download. (But should be checked nonetheless.) app_links: ^7.0.0 # 1.6 mio @@ -175,6 +176,8 @@ dependency_overrides: path: ./dependencies/exif sprintf: path: ./dependencies/sprintf + flutter_blurhash: + path: ./dependencies/flutter_blurhash dev_dependencies: build_runner: ^2.4.15 diff --git a/test/mocks/test_client.dart b/test/mocks/test_client.dart index ea750335..72e1e6ef 100644 --- a/test/mocks/test_client.dart +++ b/test/mocks/test_client.dart @@ -10,9 +10,10 @@ import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; @@ -240,7 +241,12 @@ class TestClient { await api.sendTextMessage(target.realUserId, msg.writeToBuffer(), null); } - Future sendReaction(TestClient target, String targetMessageId, String emoji, {bool remove = false}) async { + Future sendReaction( + TestClient target, + String targetMessageId, + String emoji, { + bool remove = false, + }) async { final content = pb.EncryptedContent() ..groupId = defaultGroup!.groupId ..reaction = (pb.EncryptedContent_Reaction() @@ -250,7 +256,12 @@ class TestClient { await sendEncryptedContent(target, content); } - Future sendMessageUpdate(TestClient target, String targetMessageId, pb.EncryptedContent_MessageUpdate_Type type, {String? text}) async { + Future sendMessageUpdate( + TestClient target, + String targetMessageId, + pb.EncryptedContent_MessageUpdate_Type type, { + String? text, + }) async { final update = pb.EncryptedContent_MessageUpdate() ..type = type ..senderMessageId = targetMessageId @@ -262,7 +273,11 @@ class TestClient { await sendEncryptedContent(target, content); } - Future sendMedia(TestClient target, String senderMessageId, pb.EncryptedContent_Media_Type type) async { + Future sendMedia( + TestClient target, + String senderMessageId, + pb.EncryptedContent_Media_Type type, + ) async { final content = pb.EncryptedContent() ..groupId = defaultGroup!.groupId ..media = (pb.EncryptedContent_Media() @@ -288,8 +303,10 @@ class TestClient { Future expectReaction(String messageId, String emoji) async { for (var i = 0; i < 500; i++) { final reaction = await run(() async { - final reactions = await (env.db.select(env.db.reactions)..where((t) => t.messageId.equals(messageId))).get(); - return reactions.firstWhereOrNull((r) => r.emoji == emoji); + final reactions = await (env.db.select( + env.db.reactions, + )..where((t) => t.messageId.equals(messageId))).get(); + return reactions.firstWhereOrNull((r) => r.emoji == emoji); }); if (reaction != null) return reaction; await Future.delayed(const Duration(milliseconds: 10)); diff --git a/test/mocks/user_environment.dart b/test/mocks/user_environment.dart index 264aa08b..be4e21c0 100644 --- a/test/mocks/user_environment.dart +++ b/test/mocks/user_environment.dart @@ -6,7 +6,7 @@ import 'package:drift/native.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; diff --git a/test/services/api_and_c2c_test.dart b/test/services/api_and_c2c_test.dart index 30bd5dfc..a44fe2f5 100644 --- a/test/services/api_and_c2c_test.dart +++ b/test/services/api_and_c2c_test.dart @@ -14,7 +14,7 @@ import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:workmanager/workmanager.dart'; diff --git a/test/services/backup_service_test.dart b/test/services/backup_service_test.dart index 6f3bc45a..47f842ed 100644 --- a/test/services/backup_service_test.dart +++ b/test/services/backup_service_test.dart @@ -16,7 +16,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/backup.model.dart'; import 'package:twonly/src/model/json/userdata.model.dart' hide LastBackupUploadState; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; diff --git a/test/services/group_service_test.dart b/test/services/group_service_test.dart index 29065336..7bd6712a 100644 --- a/test/services/group_service_test.dart +++ b/test/services/group_service_test.dart @@ -9,7 +9,7 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/user.service.dart'; diff --git a/test/services/key_verification_service_test.dart b/test/services/key_verification_service_test.dart index 6d6c7dae..e4949876 100644 --- a/test/services/key_verification_service_test.dart +++ b/test/services/key_verification_service_test.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/database/daos/key_verification.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/user.service.dart'; void main() { diff --git a/test/services/passwordless_recovery_service_test.dart b/test/services/passwordless_recovery_service_test.dart index 91aaa851..439e8082 100644 --- a/test/services/passwordless_recovery_service_test.dart +++ b/test/services/passwordless_recovery_service_test.dart @@ -22,7 +22,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart as api_pb; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; From 96f8bc39d08713037bd1e724517d1a5824d22500 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 20:24:14 +0200 Subject: [PATCH 17/29] start with cloud backup --- dependencies | 2 +- lib/core/bridge/wrapper/key_manager.dart | 18 + lib/core/frb_generated.dart | 150 +- lib/src/database/daos/mediafiles.dao.dart | 16 +- .../schemas/twonly_db/drift_schema_v23.json | 3161 +++++ lib/src/database/tables/mediafiles.table.dart | 13 +- lib/src/database/twonly.db.dart | 12 +- lib/src/database/twonly.db.g.dart | 145 + lib/src/database/twonly.db.steps.dart | 541 + .../generated/app_localizations.dart | 6 +- .../generated/app_localizations_de.dart | 8 +- .../generated/app_localizations_en.dart | 6 +- lib/src/localization/translations | 2 +- .../model/json/onboarding_state.model.g.dart | 6 +- lib/src/model/json/userdata.model.dart | 4 +- lib/src/model/json/userdata.model.g.dart | 2 +- .../api/websocket/client_to_server.pb.dart | 105 +- .../websocket/client_to_server.pbjson.dart | 85 +- .../api/websocket/server_to_client.pb.dart | 101 +- .../websocket/server_to_client.pbjson.dart | 126 +- lib/src/model/protobuf/client/backup.proto | 7 + .../protobuf/client/generated/backup.pb.dart | 93 + .../client/generated/backup.pbjson.dart | 24 + lib/src/services/api/api.service.dart | 71 + .../api/mediafiles/media_background.api.dart | 1 + .../mediafiles/mediafile.service.dart | 4 + .../mediafiles/thumbnail.service.dart | 2 +- lib/src/services/memories/cloud.service.dart | 0 .../services/memories/memories.service.dart | 26 +- .../memories/memories_cloud.service.dart | 394 + .../components/memory_thumbnail.comp.dart | 68 +- .../visual/views/memories/memories.view.dart | 408 +- .../memories/synchronized_viewer.view.dart | 28 +- .../settings/backup/backup_settings.view.dart | 58 + .../settings/developer/developer.view.dart | 1 + memories.md | 112 + pubspec.lock | 8 + pubspec.yaml | 1 + rust/src/bridge/wrapper/key_manager.rs | 16 + rust/src/frb_generated.rs | 96 +- rust/src/keys/main_key.rs | 32 +- test/drift/twonly_db/generated/schema.dart | 4 + .../drift/twonly_db/generated/schema_v23.dart | 11135 ++++++++++++++++ test/services/cloud_backup_test.dart | 151 + 44 files changed, 16898 insertions(+), 351 deletions(-) create mode 100644 lib/src/database/schemas/twonly_db/drift_schema_v23.json delete mode 100644 lib/src/services/memories/cloud.service.dart create mode 100644 lib/src/services/memories/memories_cloud.service.dart create mode 100644 memories.md create mode 100644 test/drift/twonly_db/generated/schema_v23.dart create mode 100644 test/services/cloud_backup_test.dart diff --git a/dependencies b/dependencies index d99dba6a..32882a31 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit d99dba6a5e0eb13d7dcb38a842e64c23a1779f1c +Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7 diff --git a/lib/core/bridge/wrapper/key_manager.dart b/lib/core/bridge/wrapper/key_manager.dart index 6e6e5095..f5f2592d 100644 --- a/lib/core/bridge/wrapper/key_manager.dart +++ b/lib/core/bridge/wrapper/key_manager.dart @@ -9,6 +9,24 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; class RustKeyManager { const RustKeyManager(); + static Future decryptCloudMediaKey({ + required List encryptedMediaKey, + required String addition, + }) => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKey( + encryptedMediaKey: encryptedMediaKey, + addition: addition, + ); + + static Future encryptCloudMediaKey({ + required List mediaKey, + required String addition, + }) => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKey( + mediaKey: mediaKey, + addition: addition, + ); + static Future getLoginToken() => RustLib.instance.api .crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken(); diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index b0b840d8..cd992e17 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -76,7 +76,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 340781866; + int get rustContentHash => 1788847092; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -205,6 +205,18 @@ abstract class RustLibApi extends BaseApi { required String password, }); + Future + crateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKey({ + required List encryptedMediaKey, + required String addition, + }); + + Future + crateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKey({ + required List mediaKey, + required String addition, + }); + Future crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken(); Future<(Uint8List, PlatformInt64)> @@ -1141,6 +1153,82 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["userId", "password"], ); + @override + Future + crateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKey({ + required List encryptedMediaKey, + required String addition, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(encryptedMediaKey, serializer); + sse_encode_String(addition, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 18, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKeyConstMeta, + argValues: [encryptedMediaKey, addition], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKeyConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_decrypt_cloud_media_key", + argNames: ["encryptedMediaKey", "addition"], + ); + + @override + Future + crateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKey({ + required List mediaKey, + required String addition, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_list_prim_u_8_loose(mediaKey, serializer); + sse_encode_String(addition, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 19, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKeyConstMeta, + argValues: [mediaKey, addition], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKeyConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_encrypt_cloud_media_key", + argNames: ["mediaKey", "addition"], + ); + @override Future crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken() { return handler.executeNormal( @@ -1150,7 +1238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 20, port: port_, ); }, @@ -1183,7 +1271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 21, port: port_, ); }, @@ -1215,7 +1303,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 22, port: port_, ); }, @@ -1250,7 +1338,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 21, + funcId: 23, port: port_, ); }, @@ -1292,7 +1380,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 24, port: port_, ); }, @@ -1336,7 +1424,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 25, port: port_, ); }, @@ -1369,7 +1457,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 26, port: port_, ); }, @@ -1401,7 +1489,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 27, port: port_, ); }, @@ -1436,7 +1524,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 26, + funcId: 28, port: port_, ); }, @@ -1468,7 +1556,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 29, port: port_, ); }, @@ -1503,7 +1591,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 28, + funcId: 30, port: port_, ); }, @@ -1540,7 +1628,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 31, port: port_, ); }, @@ -1579,7 +1667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 30, + funcId: 32, port: port_, ); }, @@ -1614,7 +1702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 33, port: port_, ); }, @@ -1650,7 +1738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 34, port: port_, ); }, @@ -1687,7 +1775,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 33, + funcId: 35, port: port_, ); }, @@ -1725,7 +1813,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 36, port: port_, ); }, @@ -1763,7 +1851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 35, + funcId: 37, port: port_, ); }, @@ -1801,7 +1889,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 38, port: port_, ); }, @@ -1840,7 +1928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 39, port: port_, ); }, @@ -1879,7 +1967,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 40, port: port_, ); }, @@ -1924,7 +2012,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 41, port: port_, ); }, @@ -1976,7 +2064,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 42, port: port_, ); }, @@ -2017,7 +2105,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 43, port: port_, ); }, @@ -2055,7 +2143,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 44, port: port_, ); }, @@ -2093,7 +2181,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 45, port: port_, ); }, @@ -2131,7 +2219,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 46, port: port_, ); }, @@ -2169,7 +2257,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 47, port: port_, ); }, @@ -2211,7 +2299,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 48, port: port_, ); }, @@ -2251,7 +2339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 49, port: port_, ); }, diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 889beb4e..040faf50 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -120,6 +120,7 @@ class MediaFilesDao extends DatabaseAccessor (t) => t.stored.equals(true) & (t.storedFileHash.isNull() | + t.blurhash.isNull() | t.hasCropAnalyzed.equals(false) | (t.hasThumbnail.equals(false) & t.type.equals(MediaType.audio.name).not()) | @@ -141,7 +142,12 @@ class MediaFilesDao extends DatabaseAccessor Stream> watchAllStoredMediaFiles() { final query = - (select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) + (select(mediaFiles)..where( + (t) => + t.stored.equals(true) | + t.cloudState.equals(CloudState.uploaded.name), + )) + .join([]) ..groupBy([ const CustomExpression( 'COALESCE(stored_file_hash, media_id)', @@ -219,4 +225,12 @@ class MediaFilesDao extends DatabaseAccessor return stats; } + + Future> getMemoriesToBackup() async { + return (select(mediaFiles)..where( + (t) => + t.stored.equals(true) & t.cloudState.equals(CloudState.none.name), + )) + .get(); + } } diff --git a/lib/src/database/schemas/twonly_db/drift_schema_v23.json b/lib/src/database/schemas/twonly_db/drift_schema_v23.json new file mode 100644 index 00000000..8e7babb3 --- /dev/null +++ b/lib/src/database/schemas/twonly_db/drift_schema_v23.json @@ -0,0 +1,3161 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "contacts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "user_id", + "getter_name": "userId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "display_name", + "getter_name": "displayName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "nick_name", + "getter_name": "nickName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "avatar_svg_compressed", + "getter_name": "avatarSvgCompressed", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_profile_counter", + "getter_name": "senderProfileCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "accepted", + "getter_name": "accepted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"accepted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"accepted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_by_user", + "getter_name": "deletedByUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_by_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_by_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "requested", + "getter_name": "requested", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requested\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requested\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "blocked", + "getter_name": "blocked", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"blocked\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"blocked\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "verified", + "getter_name": "verified", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"verified\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"verified\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "account_deleted", + "getter_name": "accountDeleted", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"account_deleted\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"account_deleted\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_version", + "getter_name": "userDiscoveryVersion", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_excluded", + "getter_name": "userDiscoveryExcluded", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_excluded\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_excluded\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "user_discovery_manual_approved", + "getter_name": "userDiscoveryManualApproved", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"user_discovery_manual_approved\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recovery_is_trusted_friend", + "getter_name": "recoveryIsTrustedFriend", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"recovery_is_trusted_friend\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"recovery_is_trusted_friend\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recovery_last_heartbeat", + "getter_name": "recoveryLastHeartbeat", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recovery_secret_share", + "getter_name": "recoverySecretShare", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recovery_contacts_secret_share", + "getter_name": "recoveryContactsSecretShare", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recovery_contacts_last_heartbeat", + "getter_name": "recoveryContactsLastHeartbeat", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "recovery_contacts_threshold", + "getter_name": "recoveryContactsThreshold", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ask_for_friend_promotions", + "getter_name": "askForFriendPromotions", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"ask_for_friend_promotions\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"ask_for_friend_promotions\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_send_counter", + "getter_name": "mediaSendCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_received_counter", + "getter_name": "mediaReceivedCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "user_id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "groups", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_group_admin", + "getter_name": "isGroupAdmin", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_group_admin\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_group_admin\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_direct_chat", + "getter_name": "isDirectChat", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_direct_chat\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_direct_chat\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pinned", + "getter_name": "pinned", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"pinned\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"pinned\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "archived", + "getter_name": "archived", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"archived\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"archived\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "joined_group", + "getter_name": "joinedGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"joined_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"joined_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "left_group", + "getter_name": "leftGroup", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"left_group\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"left_group\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "deleted_content", + "getter_name": "deletedContent", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"deleted_content\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"deleted_content\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_version_id", + "getter_name": "stateVersionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "state_encryption_key", + "getter_name": "stateEncryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "my_group_private_key", + "getter_name": "myGroupPrivateKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_name", + "getter_name": "groupName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "draft_message", + "getter_name": "draftMessage", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "total_media_counter", + "getter_name": "totalMediaCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "also_best_friend", + "getter_name": "alsoBestFriend", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"also_best_friend\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"also_best_friend\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "delete_messages_after_milliseconds", + "getter_name": "deleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('86400000')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_send", + "getter_name": "lastMessageSend", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_received", + "getter_name": "lastMessageReceived", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_counter_change", + "getter_name": "lastFlameCounterChange", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_flame_sync", + "getter_name": "lastFlameSync", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "flame_counter", + "getter_name": "flameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter", + "getter_name": "maxFlameCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "max_flame_counter_from", + "getter_name": "maxFlameCounterFrom", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message_exchange", + "getter_name": "lastMessageExchange", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id" + ] + } + }, + { + "id": 2, + "references": [], + "type": "table", + "data": { + "name": "media_files", + "was_declared_in_moor": false, + "columns": [ + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MediaType.values)", + "dart_type_name": "MediaType" + } + }, + { + "name": "upload_state", + "getter_name": "uploadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(UploadState.values)", + "dart_type_name": "UploadState" + } + }, + { + "name": "cloud_state", + "getter_name": "cloudState", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('\\'none\\'')", + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(CloudState.values)", + "dart_type_name": "CloudState" + } + }, + { + "name": "blurhash", + "getter_name": "blurhash", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_state", + "getter_name": "downloadState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(DownloadState.values)", + "dart_type_name": "DownloadState" + } + }, + { + "name": "requires_authentication", + "getter_name": "requiresAuthentication", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"requires_authentication\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"requires_authentication\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored", + "getter_name": "stored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_draft_media", + "getter_name": "isDraftMedia", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_draft_media\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_draft_media\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_favorite", + "getter_name": "isFavorite", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_favorite\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_crop_analyzed", + "getter_name": "hasCropAnalyzed", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_crop_analyzed\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_crop_analyzed\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_progressing_process", + "getter_name": "preProgressingProcess", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "reupload_requested_by", + "getter_name": "reuploadRequestedBy", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "IntListTypeConverter()", + "dart_type_name": "List" + } + }, + { + "name": "display_limit_in_milliseconds", + "getter_name": "displayLimitInMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "remove_audio", + "getter_name": "removeAudio", + "moor_type": "bool", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "CHECK (\"remove_audio\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"remove_audio\" IN (0, 1))" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_key", + "getter_name": "encryptionKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_mac", + "getter_name": "encryptionMac", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "encryption_nonce", + "getter_name": "encryptionNonce", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "stored_file_hash", + "getter_name": "storedFileHash", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "has_thumbnail", + "getter_name": "hasThumbnail", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"has_thumbnail\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"has_thumbnail\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "size_in_bytes", + "getter_name": "sizeInBytes", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at_month", + "getter_name": "createdAtMonth", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "media_id" + ] + } + }, + { + "id": 3, + "references": [ + 1, + 0, + 2 + ], + "type": "table", + "data": { + "name": "messages", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_id", + "getter_name": "mediaId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES media_files (media_id) ON DELETE SET NULL", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES media_files (media_id) ON DELETE SET NULL" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "media_files", + "column": "media_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "setNull" + } + } + ] + }, + { + "name": "additional_message_data", + "getter_name": "additionalMessageData", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_stored", + "getter_name": "mediaStored", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_stored\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_stored\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "media_reopened", + "getter_name": "mediaReopened", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"media_reopened\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"media_reopened\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download_token", + "getter_name": "downloadToken", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "quotes_message_id", + "getter_name": "quotesMessageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_deleted_from_sender", + "getter_name": "isDeletedFromSender", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_deleted_from_sender\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_deleted_from_sender\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_at", + "getter_name": "openedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "opened_by_all", + "getter_name": "openedByAll", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "modified_at", + "getter_name": "modifiedAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_user", + "getter_name": "ackByUser", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server", + "getter_name": "ackByServer", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id" + ] + } + }, + { + "id": 4, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "content", + "getter_name": "content", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 5, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "reactions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_id", + "getter_name": "senderId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "sender_id", + "emoji" + ] + } + }, + { + "id": 6, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "member_state", + "getter_name": "memberState", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MemberState.values)", + "dart_type_name": "MemberState" + } + }, + { + "name": "group_public_key", + "getter_name": "groupPublicKey", + "moor_type": "blob", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_chat_opened", + "getter_name": "lastChatOpened", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_type_indicator", + "getter_name": "lastTypeIndicator", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_message", + "getter_name": "lastMessage", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_id", + "contact_id" + ] + } + }, + { + "id": 7, + "references": [ + 0, + 3 + ], + "type": "table", + "data": { + "name": "receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "message", + "getter_name": "message", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_will_sends_receipt", + "getter_name": "contactWillSendsReceipt", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"contact_will_sends_receipt\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "will_be_retried_by_media_upload", + "getter_name": "willBeRetriedByMediaUpload", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"will_be_retried_by_media_upload\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry", + "getter_name": "markForRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "mark_for_retry_after_accepted", + "getter_name": "markForRetryAfterAccepted", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "ack_by_server_at", + "getter_name": "ackByServerAt", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "retry_count", + "getter_name": "retryCount", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_retry", + "getter_name": "lastRetry", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 8, + "references": [], + "type": "table", + "data": { + "name": "received_receipts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "receipt_id", + "getter_name": "receiptId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "receipt_id" + ] + } + }, + { + "id": 9, + "references": [], + "type": "table", + "data": { + "name": "signal_identity_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "identity_key", + "getter_name": "identityKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 10, + "references": [], + "type": "table", + "data": { + "name": "signal_pre_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "pre_key_id", + "getter_name": "preKeyId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "pre_key", + "getter_name": "preKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "pre_key_id" + ] + } + }, + { + "id": 11, + "references": [], + "type": "table", + "data": { + "name": "signal_sender_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "sender_key_name", + "getter_name": "senderKeyName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sender_key", + "getter_name": "senderKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "sender_key_name" + ] + } + }, + { + "id": 12, + "references": [], + "type": "table", + "data": { + "name": "signal_session_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "device_id", + "getter_name": "deviceId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "session_record", + "getter_name": "sessionRecord", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "device_id", + "name" + ] + } + }, + { + "id": 13, + "references": [], + "type": "table", + "data": { + "name": "signal_signed_pre_key_stores", + "was_declared_in_moor": false, + "columns": [ + { + "name": "signed_pre_key_id", + "getter_name": "signedPreKeyId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "signed_pre_key", + "getter_name": "signedPreKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "signed_pre_key_id" + ] + } + }, + { + "id": 14, + "references": [ + 3, + 0 + ], + "type": "table", + "data": { + "name": "message_actions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "message_id", + "getter_name": "messageId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES messages (message_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES messages (message_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "messages", + "column": "message_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(MessageActionType.values)", + "dart_type_name": "MessageActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "message_id", + "contact_id", + "type" + ] + } + }, + { + "id": 15, + "references": [ + 1, + 0 + ], + "type": "table", + "data": { + "name": "group_histories", + "was_declared_in_moor": false, + "columns": [ + { + "name": "group_history_id", + "getter_name": "groupHistoryId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "affected_contact_id", + "getter_name": "affectedContactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "old_group_name", + "getter_name": "oldGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_group_name", + "getter_name": "newGroupName", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "new_delete_messages_after_milliseconds", + "getter_name": "newDeleteMessagesAfterMilliseconds", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(GroupActionType.values)", + "dart_type_name": "GroupActionType" + } + }, + { + "name": "action_at", + "getter_name": "actionAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "group_history_id" + ] + } + }, + { + "id": 16, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "key_verifications", + "was_declared_in_moor": false, + "columns": [ + { + "name": "verification_id", + "getter_name": "verificationId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(VerificationType.values)", + "dart_type_name": "VerificationType" + } + }, + { + "name": "verified_by", + "getter_name": "verifiedBy", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 17, + "references": [], + "type": "table", + "data": { + "name": "verification_tokens", + "was_declared_in_moor": false, + "columns": [ + { + "name": "token_id", + "getter_name": "tokenId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "token", + "getter_name": "token", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 18, + "references": [], + "type": "table", + "data": { + "name": "user_discovery_announced_users", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announced_public_key", + "getter_name": "announcedPublicKey", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "username", + "getter_name": "username", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "was_shown_to_the_user", + "getter_name": "wasShownToTheUser", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"was_shown_to_the_user\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"was_shown_to_the_user\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "is_hidden", + "getter_name": "isHidden", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_hidden\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "was_asked_friends", + "getter_name": "wasAskedFriends", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"was_asked_friends\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"was_asked_friends\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id" + ] + } + }, + { + "id": 19, + "references": [ + 18, + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_user_relations", + "was_declared_in_moor": false, + "columns": [ + { + "name": "announced_user_id", + "getter_name": "announcedUserId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "user_discovery_announced_users", + "column": "announced_user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "announced_user_id", + "from_contact_id" + ] + } + }, + { + "id": 20, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_other_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "from_contact_id", + "getter_name": "fromContactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion_id", + "getter_name": "promotionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_id", + "getter_name": "publicId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "threshold", + "getter_name": "threshold", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "announcement_share", + "getter_name": "announcementShare", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "public_key_verified_timestamp", + "getter_name": "publicKeyVerifiedTimestamp", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "from_contact_id", + "public_id" + ] + } + }, + { + "id": 21, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_own_promotions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "version_id", + "getter_name": "versionId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "promotion", + "getter_name": "promotion", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 22, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "user_discovery_shares", + "was_declared_in_moor": false, + "columns": [ + { + "name": "share_id", + "getter_name": "shareId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "share", + "getter_name": "share", + "moor_type": "blob", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "contact_id", + "getter_name": "contactId", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "defaultConstraints": "REFERENCES contacts (user_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES contacts (user_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "contacts", + "column": "user_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 23, + "references": [], + "type": "table", + "data": { + "name": "shortcuts", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "emoji", + "getter_name": "emoji", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "UNIQUE", + "dialectAwareDefaultConstraints": { + "sqlite": "UNIQUE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "unique" + ] + }, + { + "name": "usage_counter", + "getter_name": "usageCounter", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 24, + "references": [ + 23, + 1 + ], + "type": "table", + "data": { + "name": "shortcut_members", + "was_declared_in_moor": false, + "columns": [ + { + "name": "shortcut_id", + "getter_name": "shortcutId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES shortcuts (id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES shortcuts (id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "shortcuts", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + }, + { + "name": "group_id", + "getter_name": "groupId", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES \"groups\" (group_id) ON DELETE CASCADE" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "groups", + "column": "group_id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": "cascade" + } + } + ] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "shortcut_id", + "group_id" + ] + } + } + ], + "fixed_sql": [ + { + "name": "contacts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"contacts\" (\"user_id\" INTEGER NOT NULL, \"username\" TEXT NOT NULL, \"display_name\" TEXT NULL, \"nick_name\" TEXT NULL, \"avatar_svg_compressed\" BLOB NULL, \"sender_profile_counter\" INTEGER NOT NULL DEFAULT 0, \"accepted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"accepted\" IN (0, 1)), \"deleted_by_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_by_user\" IN (0, 1)), \"requested\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requested\" IN (0, 1)), \"blocked\" INTEGER NOT NULL DEFAULT 0 CHECK (\"blocked\" IN (0, 1)), \"verified\" INTEGER NOT NULL DEFAULT 0 CHECK (\"verified\" IN (0, 1)), \"account_deleted\" INTEGER NOT NULL DEFAULT 0 CHECK (\"account_deleted\" IN (0, 1)), \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"user_discovery_version\" BLOB NULL, \"user_discovery_excluded\" INTEGER NOT NULL DEFAULT 0 CHECK (\"user_discovery_excluded\" IN (0, 1)), \"user_discovery_manual_approved\" INTEGER NULL DEFAULT 0 CHECK (\"user_discovery_manual_approved\" IN (0, 1)), \"recovery_is_trusted_friend\" INTEGER NOT NULL DEFAULT 0 CHECK (\"recovery_is_trusted_friend\" IN (0, 1)), \"recovery_last_heartbeat\" INTEGER NULL, \"recovery_secret_share\" BLOB NULL, \"recovery_contacts_secret_share\" BLOB NULL, \"recovery_contacts_last_heartbeat\" INTEGER NULL, \"recovery_contacts_threshold\" INTEGER NULL, \"ask_for_friend_promotions\" INTEGER NULL CHECK (\"ask_for_friend_promotions\" IN (0, 1)), \"media_send_counter\" INTEGER NOT NULL DEFAULT 0, \"media_received_counter\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"user_id\"));" + } + ] + }, + { + "name": "groups", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"groups\" (\"group_id\" TEXT NOT NULL, \"is_group_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_group_admin\" IN (0, 1)), \"is_direct_chat\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_direct_chat\" IN (0, 1)), \"pinned\" INTEGER NOT NULL DEFAULT 0 CHECK (\"pinned\" IN (0, 1)), \"archived\" INTEGER NOT NULL DEFAULT 0 CHECK (\"archived\" IN (0, 1)), \"joined_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"joined_group\" IN (0, 1)), \"left_group\" INTEGER NOT NULL DEFAULT 0 CHECK (\"left_group\" IN (0, 1)), \"deleted_content\" INTEGER NOT NULL DEFAULT 0 CHECK (\"deleted_content\" IN (0, 1)), \"state_version_id\" INTEGER NOT NULL DEFAULT 0, \"state_encryption_key\" BLOB NULL, \"my_group_private_key\" BLOB NULL, \"group_name\" TEXT NOT NULL, \"draft_message\" TEXT NULL, \"total_media_counter\" INTEGER NOT NULL DEFAULT 0, \"also_best_friend\" INTEGER NOT NULL DEFAULT 0 CHECK (\"also_best_friend\" IN (0, 1)), \"delete_messages_after_milliseconds\" INTEGER NOT NULL DEFAULT 86400000, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"last_message_send\" INTEGER NULL, \"last_message_received\" INTEGER NULL, \"last_flame_counter_change\" INTEGER NULL, \"last_flame_sync\" INTEGER NULL, \"flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter\" INTEGER NOT NULL DEFAULT 0, \"max_flame_counter_from\" INTEGER NULL, \"last_message_exchange\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\"));" + } + ] + }, + { + "name": "media_files", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"media_files\" (\"media_id\" TEXT NOT NULL, \"type\" TEXT NOT NULL, \"upload_state\" TEXT NULL, \"cloud_state\" TEXT NOT NULL DEFAULT 'none', \"blurhash\" TEXT NULL, \"download_state\" TEXT NULL, \"requires_authentication\" INTEGER NOT NULL DEFAULT 0 CHECK (\"requires_authentication\" IN (0, 1)), \"stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"stored\" IN (0, 1)), \"is_draft_media\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_draft_media\" IN (0, 1)), \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"has_crop_analyzed\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_crop_analyzed\" IN (0, 1)), \"pre_progressing_process\" INTEGER NULL, \"reupload_requested_by\" TEXT NULL, \"display_limit_in_milliseconds\" INTEGER NULL, \"remove_audio\" INTEGER NULL CHECK (\"remove_audio\" IN (0, 1)), \"download_token\" BLOB NULL, \"encryption_key\" BLOB NULL, \"encryption_mac\" BLOB NULL, \"encryption_nonce\" BLOB NULL, \"stored_file_hash\" BLOB NULL, \"has_thumbnail\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_thumbnail\" IN (0, 1)), \"size_in_bytes\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"created_at_month\" TEXT NULL, PRIMARY KEY (\"media_id\"));" + } + ] + }, + { + "name": "messages", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"messages\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"message_id\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id), \"type\" TEXT NOT NULL, \"content\" TEXT NULL, \"media_id\" TEXT NULL REFERENCES media_files (media_id) ON DELETE SET NULL, \"additional_message_data\" BLOB NULL, \"media_stored\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_stored\" IN (0, 1)), \"media_reopened\" INTEGER NOT NULL DEFAULT 0 CHECK (\"media_reopened\" IN (0, 1)), \"download_token\" BLOB NULL, \"quotes_message_id\" TEXT NULL, \"is_deleted_from_sender\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_deleted_from_sender\" IN (0, 1)), \"opened_at\" INTEGER NULL, \"opened_by_all\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), \"modified_at\" INTEGER NULL, \"ack_by_user\" INTEGER NULL, \"ack_by_server\" INTEGER NULL, PRIMARY KEY (\"message_id\"));" + } + ] + }, + { + "name": "message_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_histories\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"content\" TEXT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "reactions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"reactions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"emoji\" TEXT NOT NULL, \"sender_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"sender_id\", \"emoji\"));" + } + ] + }, + { + "name": "group_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_members\" (\"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id), \"member_state\" TEXT NULL, \"group_public_key\" BLOB NULL, \"last_chat_opened\" INTEGER NULL, \"last_type_indicator\" INTEGER NULL, \"last_message\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_id\", \"contact_id\"));" + } + ] + }, + { + "name": "receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"receipts\" (\"receipt_id\" TEXT NOT NULL, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"message_id\" TEXT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"message\" BLOB NOT NULL, \"contact_will_sends_receipt\" INTEGER NOT NULL DEFAULT 1 CHECK (\"contact_will_sends_receipt\" IN (0, 1)), \"will_be_retried_by_media_upload\" INTEGER NOT NULL DEFAULT 0 CHECK (\"will_be_retried_by_media_upload\" IN (0, 1)), \"mark_for_retry\" INTEGER NULL, \"mark_for_retry_after_accepted\" INTEGER NULL, \"ack_by_server_at\" INTEGER NULL, \"retry_count\" INTEGER NOT NULL DEFAULT 0, \"last_retry\" INTEGER NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "received_receipts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"received_receipts\" (\"receipt_id\" TEXT NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"receipt_id\"));" + } + ] + }, + { + "name": "signal_identity_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_identity_key_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"identity_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "signal_pre_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_pre_key_stores\" (\"pre_key_id\" INTEGER NOT NULL, \"pre_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"pre_key_id\"));" + } + ] + }, + { + "name": "signal_sender_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_sender_key_stores\" (\"sender_key_name\" TEXT NOT NULL, \"sender_key\" BLOB NOT NULL, PRIMARY KEY (\"sender_key_name\"));" + } + ] + }, + { + "name": "signal_session_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_session_stores\" (\"device_id\" INTEGER NOT NULL, \"name\" TEXT NOT NULL, \"session_record\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"device_id\", \"name\"));" + } + ] + }, + { + "name": "signal_signed_pre_key_stores", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"signal_signed_pre_key_stores\" (\"signed_pre_key_id\" INTEGER NOT NULL, \"signed_pre_key\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"signed_pre_key_id\"));" + } + ] + }, + { + "name": "message_actions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"message_actions\" (\"message_id\" TEXT NOT NULL REFERENCES messages (message_id) ON DELETE CASCADE, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"message_id\", \"contact_id\", \"type\"));" + } + ] + }, + { + "name": "group_histories", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"group_histories\" (\"group_history_id\" TEXT NOT NULL, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id), \"affected_contact_id\" INTEGER NULL, \"old_group_name\" TEXT NULL, \"new_group_name\" TEXT NULL, \"new_delete_messages_after_milliseconds\" INTEGER NULL, \"type\" TEXT NOT NULL, \"action_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY (\"group_history_id\"));" + } + ] + }, + { + "name": "key_verifications", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"key_verifications\" (\"verification_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"type\" TEXT NOT NULL, \"verified_by\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "verification_tokens", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"verification_tokens\" (\"token_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"token\" BLOB NOT NULL, \"created_at\" INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)));" + } + ] + }, + { + "name": "user_discovery_announced_users", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_announced_users\" (\"announced_user_id\" INTEGER NOT NULL, \"announced_public_key\" BLOB NOT NULL, \"public_id\" INTEGER NOT NULL UNIQUE, \"username\" TEXT NULL, \"was_shown_to_the_user\" INTEGER NOT NULL DEFAULT 0 CHECK (\"was_shown_to_the_user\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_hidden\" IN (0, 1)), \"was_asked_friends\" INTEGER NOT NULL DEFAULT 0 CHECK (\"was_asked_friends\" IN (0, 1)), PRIMARY KEY (\"announced_user_id\"));" + } + ] + }, + { + "name": "user_discovery_user_relations", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_user_relations\" (\"announced_user_id\" INTEGER NOT NULL REFERENCES user_discovery_announced_users (announced_user_id) ON DELETE CASCADE, \"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"announced_user_id\", \"from_contact_id\"));" + } + ] + }, + { + "name": "user_discovery_other_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_other_promotions\" (\"from_contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion_id\" INTEGER NOT NULL, \"public_id\" INTEGER NOT NULL, \"threshold\" INTEGER NOT NULL, \"announcement_share\" BLOB NOT NULL, \"public_key_verified_timestamp\" INTEGER NULL, PRIMARY KEY (\"from_contact_id\", \"public_id\"));" + } + ] + }, + { + "name": "user_discovery_own_promotions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_own_promotions\" (\"version_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"contact_id\" INTEGER NOT NULL REFERENCES contacts (user_id) ON DELETE CASCADE, \"promotion\" BLOB NOT NULL);" + } + ] + }, + { + "name": "user_discovery_shares", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"user_discovery_shares\" (\"share_id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"share\" BLOB NOT NULL, \"contact_id\" INTEGER NULL REFERENCES contacts (user_id) ON DELETE CASCADE);" + } + ] + }, + { + "name": "shortcuts", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcuts\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"emoji\" TEXT NOT NULL UNIQUE, \"usage_counter\" INTEGER NOT NULL DEFAULT 0);" + } + ] + }, + { + "name": "shortcut_members", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"shortcut_members\" (\"shortcut_id\" INTEGER NOT NULL REFERENCES shortcuts (id) ON DELETE CASCADE, \"group_id\" TEXT NOT NULL REFERENCES \"groups\" (group_id) ON DELETE CASCADE, PRIMARY KEY (\"shortcut_id\", \"group_id\"));" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 23ed91cf..b1191e7b 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -8,6 +8,12 @@ enum MediaType { audio, } +enum CloudState { + none, + pending, + uploaded, +} + enum UploadState { // Image/Video was taken. A database entry was created to track it... initialized, @@ -43,6 +49,9 @@ class MediaFiles extends Table { TextColumn get type => textEnum()(); TextColumn get uploadState => textEnum().nullable()(); + TextColumn get cloudState => + textEnum().withDefault(const Constant('none'))(); + TextColumn get blurhash => text().nullable()(); TextColumn get downloadState => textEnum().nullable()(); BoolColumn get requiresAuthentication => @@ -69,15 +78,13 @@ class MediaFiles extends Table { BlobColumn get storedFileHash => blob().nullable()(); - BoolColumn get hasThumbnail => - boolean().withDefault(const Constant(false))(); + BoolColumn get hasThumbnail => boolean().withDefault(const Constant(false))(); IntColumn get sizeInBytes => integer().nullable()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); TextColumn get createdAtMonth => text().nullable()(); - @override Set get primaryKey => {mediaId}; } diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 77c01266..34850cb3 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -82,7 +82,7 @@ class TwonlyDB extends _$TwonlyDB { TwonlyDB.forTesting(DatabaseConnection super.connection); @override - int get schemaVersion => 22; + int get schemaVersion => 23; static QueryExecutor _openConnection() { final connection = driftDatabase( @@ -275,6 +275,16 @@ class TwonlyDB extends _$TwonlyDB { schema.contacts.recoveryContactsThreshold, ); }, + from22To23: (m, schema) async { + await m.addColumn( + schema.mediaFiles, + schema.mediaFiles.cloudState, + ); + await m.addColumn( + schema.mediaFiles, + schema.mediaFiles.blurhash, + ); + }, )(m, from, to); }, ); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 504d9ea0..e0634250 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -3094,6 +3094,27 @@ class $MediaFilesTable extends MediaFiles requiredDuringInsert: false, ).withConverter($MediaFilesTable.$converteruploadStaten); @override + late final GeneratedColumnWithTypeConverter cloudState = + GeneratedColumn( + 'cloud_state', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('none'), + ).withConverter($MediaFilesTable.$convertercloudState); + static const VerificationMeta _blurhashMeta = const VerificationMeta( + 'blurhash', + ); + @override + late final GeneratedColumn blurhash = GeneratedColumn( + 'blurhash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override late final GeneratedColumnWithTypeConverter downloadState = GeneratedColumn( 'download_state', @@ -3333,6 +3354,8 @@ class $MediaFilesTable extends MediaFiles mediaId, type, uploadState, + cloudState, + blurhash, downloadState, requiresAuthentication, stored, @@ -3373,6 +3396,12 @@ class $MediaFilesTable extends MediaFiles } else if (isInserting) { context.missing(_mediaIdMeta); } + if (data.containsKey('blurhash')) { + context.handle( + _blurhashMeta, + blurhash.isAcceptableOrUnknown(data['blurhash']!, _blurhashMeta), + ); + } if (data.containsKey('requires_authentication')) { context.handle( _requiresAuthenticationMeta, @@ -3542,6 +3571,16 @@ class $MediaFilesTable extends MediaFiles data['${effectivePrefix}upload_state'], ), ), + cloudState: $MediaFilesTable.$convertercloudState.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_state'], + )!, + ), + blurhash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}blurhash'], + ), downloadState: $MediaFilesTable.$converterdownloadStaten.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, @@ -3637,6 +3676,8 @@ class $MediaFilesTable extends MediaFiles const EnumNameConverter(UploadState.values); static JsonTypeConverter2 $converteruploadStaten = JsonTypeConverter2.asNullable($converteruploadState); + static JsonTypeConverter2 $convertercloudState = + const EnumNameConverter(CloudState.values); static JsonTypeConverter2 $converterdownloadState = const EnumNameConverter( DownloadState.values, @@ -3655,6 +3696,8 @@ class MediaFile extends DataClass implements Insertable { final String mediaId; final MediaType type; final UploadState? uploadState; + final CloudState cloudState; + final String? blurhash; final DownloadState? downloadState; final bool requiresAuthentication; final bool stored; @@ -3678,6 +3721,8 @@ class MediaFile extends DataClass implements Insertable { required this.mediaId, required this.type, this.uploadState, + required this.cloudState, + this.blurhash, this.downloadState, required this.requiresAuthentication, required this.stored, @@ -3712,6 +3757,14 @@ class MediaFile extends DataClass implements Insertable { $MediaFilesTable.$converteruploadStaten.toSql(uploadState), ); } + { + map['cloud_state'] = Variable( + $MediaFilesTable.$convertercloudState.toSql(cloudState), + ); + } + if (!nullToAbsent || blurhash != null) { + map['blurhash'] = Variable(blurhash); + } if (!nullToAbsent || downloadState != null) { map['download_state'] = Variable( $MediaFilesTable.$converterdownloadStaten.toSql(downloadState), @@ -3773,6 +3826,10 @@ class MediaFile extends DataClass implements Insertable { uploadState: uploadState == null && nullToAbsent ? const Value.absent() : Value(uploadState), + cloudState: Value(cloudState), + blurhash: blurhash == null && nullToAbsent + ? const Value.absent() + : Value(blurhash), downloadState: downloadState == null && nullToAbsent ? const Value.absent() : Value(downloadState), @@ -3833,6 +3890,10 @@ class MediaFile extends DataClass implements Insertable { uploadState: $MediaFilesTable.$converteruploadStaten.fromJson( serializer.fromJson(json['uploadState']), ), + cloudState: $MediaFilesTable.$convertercloudState.fromJson( + serializer.fromJson(json['cloudState']), + ), + blurhash: serializer.fromJson(json['blurhash']), downloadState: $MediaFilesTable.$converterdownloadStaten.fromJson( serializer.fromJson(json['downloadState']), ), @@ -3875,6 +3936,10 @@ class MediaFile extends DataClass implements Insertable { 'uploadState': serializer.toJson( $MediaFilesTable.$converteruploadStaten.toJson(uploadState), ), + 'cloudState': serializer.toJson( + $MediaFilesTable.$convertercloudState.toJson(cloudState), + ), + 'blurhash': serializer.toJson(blurhash), 'downloadState': serializer.toJson( $MediaFilesTable.$converterdownloadStaten.toJson(downloadState), ), @@ -3905,6 +3970,8 @@ class MediaFile extends DataClass implements Insertable { String? mediaId, MediaType? type, Value uploadState = const Value.absent(), + CloudState? cloudState, + Value blurhash = const Value.absent(), Value downloadState = const Value.absent(), bool? requiresAuthentication, bool? stored, @@ -3928,6 +3995,8 @@ class MediaFile extends DataClass implements Insertable { mediaId: mediaId ?? this.mediaId, type: type ?? this.type, uploadState: uploadState.present ? uploadState.value : this.uploadState, + cloudState: cloudState ?? this.cloudState, + blurhash: blurhash.present ? blurhash.value : this.blurhash, downloadState: downloadState.present ? downloadState.value : this.downloadState, @@ -3976,6 +4045,10 @@ class MediaFile extends DataClass implements Insertable { uploadState: data.uploadState.present ? data.uploadState.value : this.uploadState, + cloudState: data.cloudState.present + ? data.cloudState.value + : this.cloudState, + blurhash: data.blurhash.present ? data.blurhash.value : this.blurhash, downloadState: data.downloadState.present ? data.downloadState.value : this.downloadState, @@ -4038,6 +4111,8 @@ class MediaFile extends DataClass implements Insertable { ..write('mediaId: $mediaId, ') ..write('type: $type, ') ..write('uploadState: $uploadState, ') + ..write('cloudState: $cloudState, ') + ..write('blurhash: $blurhash, ') ..write('downloadState: $downloadState, ') ..write('requiresAuthentication: $requiresAuthentication, ') ..write('stored: $stored, ') @@ -4066,6 +4141,8 @@ class MediaFile extends DataClass implements Insertable { mediaId, type, uploadState, + cloudState, + blurhash, downloadState, requiresAuthentication, stored, @@ -4093,6 +4170,8 @@ class MediaFile extends DataClass implements Insertable { other.mediaId == this.mediaId && other.type == this.type && other.uploadState == this.uploadState && + other.cloudState == this.cloudState && + other.blurhash == this.blurhash && other.downloadState == this.downloadState && other.requiresAuthentication == this.requiresAuthentication && other.stored == this.stored && @@ -4124,6 +4203,8 @@ class MediaFilesCompanion extends UpdateCompanion { final Value mediaId; final Value type; final Value uploadState; + final Value cloudState; + final Value blurhash; final Value downloadState; final Value requiresAuthentication; final Value stored; @@ -4148,6 +4229,8 @@ class MediaFilesCompanion extends UpdateCompanion { this.mediaId = const Value.absent(), this.type = const Value.absent(), this.uploadState = const Value.absent(), + this.cloudState = const Value.absent(), + this.blurhash = const Value.absent(), this.downloadState = const Value.absent(), this.requiresAuthentication = const Value.absent(), this.stored = const Value.absent(), @@ -4173,6 +4256,8 @@ class MediaFilesCompanion extends UpdateCompanion { required String mediaId, required MediaType type, this.uploadState = const Value.absent(), + this.cloudState = const Value.absent(), + this.blurhash = const Value.absent(), this.downloadState = const Value.absent(), this.requiresAuthentication = const Value.absent(), this.stored = const Value.absent(), @@ -4199,6 +4284,8 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? mediaId, Expression? type, Expression? uploadState, + Expression? cloudState, + Expression? blurhash, Expression? downloadState, Expression? requiresAuthentication, Expression? stored, @@ -4224,6 +4311,8 @@ class MediaFilesCompanion extends UpdateCompanion { if (mediaId != null) 'media_id': mediaId, if (type != null) 'type': type, if (uploadState != null) 'upload_state': uploadState, + if (cloudState != null) 'cloud_state': cloudState, + if (blurhash != null) 'blurhash': blurhash, if (downloadState != null) 'download_state': downloadState, if (requiresAuthentication != null) 'requires_authentication': requiresAuthentication, @@ -4255,6 +4344,8 @@ class MediaFilesCompanion extends UpdateCompanion { Value? mediaId, Value? type, Value? uploadState, + Value? cloudState, + Value? blurhash, Value? downloadState, Value? requiresAuthentication, Value? stored, @@ -4280,6 +4371,8 @@ class MediaFilesCompanion extends UpdateCompanion { mediaId: mediaId ?? this.mediaId, type: type ?? this.type, uploadState: uploadState ?? this.uploadState, + cloudState: cloudState ?? this.cloudState, + blurhash: blurhash ?? this.blurhash, downloadState: downloadState ?? this.downloadState, requiresAuthentication: requiresAuthentication ?? this.requiresAuthentication, @@ -4322,6 +4415,14 @@ class MediaFilesCompanion extends UpdateCompanion { $MediaFilesTable.$converteruploadStaten.toSql(uploadState.value), ); } + if (cloudState.present) { + map['cloud_state'] = Variable( + $MediaFilesTable.$convertercloudState.toSql(cloudState.value), + ); + } + if (blurhash.present) { + map['blurhash'] = Variable(blurhash.value); + } if (downloadState.present) { map['download_state'] = Variable( $MediaFilesTable.$converterdownloadStaten.toSql(downloadState.value), @@ -4403,6 +4504,8 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('mediaId: $mediaId, ') ..write('type: $type, ') ..write('uploadState: $uploadState, ') + ..write('cloudState: $cloudState, ') + ..write('blurhash: $blurhash, ') ..write('downloadState: $downloadState, ') ..write('requiresAuthentication: $requiresAuthentication, ') ..write('stored: $stored, ') @@ -16080,6 +16183,8 @@ typedef $$MediaFilesTableCreateCompanionBuilder = required String mediaId, required MediaType type, Value uploadState, + Value cloudState, + Value blurhash, Value downloadState, Value requiresAuthentication, Value stored, @@ -16106,6 +16211,8 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = Value mediaId, Value type, Value uploadState, + Value cloudState, + Value blurhash, Value downloadState, Value requiresAuthentication, Value stored, @@ -16177,6 +16284,17 @@ class $$MediaFilesTableFilterComposer builder: (column) => ColumnWithTypeConverterFilters(column), ); + ColumnWithTypeConverterFilters + get cloudState => $composableBuilder( + column: $table.cloudState, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get blurhash => $composableBuilder( + column: $table.blurhash, + builder: (column) => ColumnFilters(column), + ); + ColumnWithTypeConverterFilters get downloadState => $composableBuilder( column: $table.downloadState, @@ -16324,6 +16442,16 @@ class $$MediaFilesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get cloudState => $composableBuilder( + column: $table.cloudState, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get blurhash => $composableBuilder( + column: $table.blurhash, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get downloadState => $composableBuilder( column: $table.downloadState, builder: (column) => ColumnOrderings(column), @@ -16441,6 +16569,15 @@ class $$MediaFilesTableAnnotationComposer builder: (column) => column, ); + GeneratedColumnWithTypeConverter get cloudState => + $composableBuilder( + column: $table.cloudState, + builder: (column) => column, + ); + + GeneratedColumn get blurhash => + $composableBuilder(column: $table.blurhash, builder: (column) => column); + GeneratedColumnWithTypeConverter get downloadState => $composableBuilder( column: $table.downloadState, @@ -16591,6 +16728,8 @@ class $$MediaFilesTableTableManager Value mediaId = const Value.absent(), Value type = const Value.absent(), Value uploadState = const Value.absent(), + Value cloudState = const Value.absent(), + Value blurhash = const Value.absent(), Value downloadState = const Value.absent(), Value requiresAuthentication = const Value.absent(), Value stored = const Value.absent(), @@ -16615,6 +16754,8 @@ class $$MediaFilesTableTableManager mediaId: mediaId, type: type, uploadState: uploadState, + cloudState: cloudState, + blurhash: blurhash, downloadState: downloadState, requiresAuthentication: requiresAuthentication, stored: stored, @@ -16641,6 +16782,8 @@ class $$MediaFilesTableTableManager required String mediaId, required MediaType type, Value uploadState = const Value.absent(), + Value cloudState = const Value.absent(), + Value blurhash = const Value.absent(), Value downloadState = const Value.absent(), Value requiresAuthentication = const Value.absent(), Value stored = const Value.absent(), @@ -16665,6 +16808,8 @@ class $$MediaFilesTableTableManager mediaId: mediaId, type: type, uploadState: uploadState, + cloudState: cloudState, + blurhash: blurhash, downloadState: downloadState, requiresAuthentication: requiresAuthentication, stored: stored, diff --git a/lib/src/database/twonly.db.steps.dart b/lib/src/database/twonly.db.steps.dart index daf37fed..c2e28613 100644 --- a/lib/src/database/twonly.db.steps.dart +++ b/lib/src/database/twonly.db.steps.dart @@ -11600,6 +11600,539 @@ i1.GeneratedColumn _column_254(String aliasedName) => type: i1.DriftSqlType.int, $customConstraints: 'NULL', ); + +final class Schema23 extends i0.VersionedSchema { + Schema23({required super.database}) : super(version: 23); + @override + late final List entities = [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalSignedPreKeyStores, + messageActions, + groupHistories, + keyVerifications, + verificationTokens, + userDiscoveryAnnouncedUsers, + userDiscoveryUserRelations, + userDiscoveryOtherPromotions, + userDiscoveryOwnPromotions, + userDiscoveryShares, + shortcuts, + shortcutMembers, + ]; + late final Shape57 contacts = Shape57( + source: i0.VersionedTable( + entityName: 'contacts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(user_id)'], + columns: [ + _column_106, + _column_107, + _column_108, + _column_109, + _column_110, + _column_111, + _column_112, + _column_113, + _column_114, + _column_115, + _column_116, + _column_117, + _column_118, + _column_211, + _column_212, + _column_213, + _column_249, + _column_250, + _column_251, + _column_252, + _column_253, + _column_254, + _column_247, + _column_214, + _column_215, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape23 groups = Shape23( + source: i0.VersionedTable( + entityName: 'groups', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id)'], + columns: [ + _column_119, + _column_120, + _column_121, + _column_122, + _column_123, + _column_124, + _column_125, + _column_126, + _column_127, + _column_128, + _column_129, + _column_130, + _column_131, + _column_132, + _column_133, + _column_134, + _column_118, + _column_135, + _column_136, + _column_137, + _column_138, + _column_139, + _column_140, + _column_141, + _column_142, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape58 mediaFiles = Shape58( + source: i0.VersionedTable( + entityName: 'media_files', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(media_id)'], + columns: [ + _column_143, + _column_144, + _column_145, + _column_255, + _column_256, + _column_146, + _column_147, + _column_148, + _column_149, + _column_239, + _column_240, + _column_207, + _column_150, + _column_151, + _column_152, + _column_153, + _column_154, + _column_155, + _column_156, + _column_157, + _column_244, + _column_245, + _column_118, + _column_241, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 messages = Shape25( + source: i0.VersionedTable( + entityName: 'messages', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id)'], + columns: [ + _column_158, + _column_159, + _column_160, + _column_144, + _column_161, + _column_162, + _column_163, + _column_164, + _column_165, + _column_153, + _column_166, + _column_167, + _column_168, + _column_169, + _column_118, + _column_170, + _column_171, + _column_172, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 messageHistories = Shape26( + source: i0.VersionedTable( + entityName: 'message_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_173, + _column_174, + _column_175, + _column_161, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 reactions = Shape27( + source: i0.VersionedTable( + entityName: 'reactions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'], + columns: [_column_174, _column_176, _column_177, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape38 groupMembers = Shape38( + source: i0.VersionedTable( + entityName: 'group_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_id, contact_id)'], + columns: [ + _column_158, + _column_178, + _column_179, + _column_180, + _column_209, + _column_210, + _column_181, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape37 receipts = Shape37( + source: i0.VersionedTable( + entityName: 'receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [ + _column_182, + _column_183, + _column_184, + _column_185, + _column_186, + _column_208, + _column_187, + _column_188, + _column_189, + _column_190, + _column_191, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape30 receivedReceipts = Shape30( + source: i0.VersionedTable( + entityName: 'received_receipts', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(receipt_id)'], + columns: [_column_182, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape31 signalIdentityKeyStores = Shape31( + source: i0.VersionedTable( + entityName: 'signal_identity_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_194, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape32 signalPreKeyStores = Shape32( + source: i0.VersionedTable( + entityName: 'signal_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(pre_key_id)'], + columns: [_column_195, _column_196, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 signalSenderKeyStores = Shape11( + source: i0.VersionedTable( + entityName: 'signal_sender_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(sender_key_name)'], + columns: [_column_197, _column_198], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape33 signalSessionStores = Shape33( + source: i0.VersionedTable( + entityName: 'signal_session_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(device_id, name)'], + columns: [_column_192, _column_193, _column_199, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape50 signalSignedPreKeyStores = Shape50( + source: i0.VersionedTable( + entityName: 'signal_signed_pre_key_stores', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'], + columns: [_column_242, _column_243, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape34 messageActions = Shape34( + source: i0.VersionedTable( + entityName: 'message_actions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'], + columns: [_column_174, _column_183, _column_144, _column_200], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape35 groupHistories = Shape35( + source: i0.VersionedTable( + entityName: 'group_histories', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(group_history_id)'], + columns: [ + _column_201, + _column_158, + _column_202, + _column_203, + _column_204, + _column_205, + _column_206, + _column_144, + _column_200, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape54 keyVerifications = Shape54( + source: i0.VersionedTable( + entityName: 'key_verifications', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_216, + _column_183, + _column_144, + _column_248, + _column_118, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape41 verificationTokens = Shape41( + source: i0.VersionedTable( + entityName: 'verification_tokens', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_217, _column_218, _column_118], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape52 userDiscoveryAnnouncedUsers = Shape52( + source: i0.VersionedTable( + entityName: 'user_discovery_announced_users', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id)'], + columns: [ + _column_219, + _column_220, + _column_221, + _column_222, + _column_223, + _column_224, + _column_246, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape43 userDiscoveryUserRelations = Shape43( + source: i0.VersionedTable( + entityName: 'user_discovery_user_relations', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'], + columns: [_column_225, _column_226, _column_227], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape44 userDiscoveryOtherPromotions = Shape44( + source: i0.VersionedTable( + entityName: 'user_discovery_other_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'], + columns: [ + _column_226, + _column_228, + _column_229, + _column_230, + _column_231, + _column_227, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape45 userDiscoveryOwnPromotions = Shape45( + source: i0.VersionedTable( + entityName: 'user_discovery_own_promotions', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_232, _column_183, _column_233], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape46 userDiscoveryShares = Shape46( + source: i0.VersionedTable( + entityName: 'user_discovery_shares', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_234, _column_235, _column_175], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape47 shortcuts = Shape47( + source: i0.VersionedTable( + entityName: 'shortcuts', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [_column_173, _column_236, _column_237], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape48 shortcutMembers = Shape48( + source: i0.VersionedTable( + entityName: 'shortcut_members', + withoutRowId: false, + isStrict: false, + tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'], + columns: [_column_238, _column_158], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape58 extends i0.VersionedTable { + Shape58({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get mediaId => + columnsByName['media_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get type => + columnsByName['type']! as i1.GeneratedColumn; + i1.GeneratedColumn get uploadState => + columnsByName['upload_state']! as i1.GeneratedColumn; + i1.GeneratedColumn get cloudState => + columnsByName['cloud_state']! as i1.GeneratedColumn; + i1.GeneratedColumn get blurhash => + columnsByName['blurhash']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadState => + columnsByName['download_state']! as i1.GeneratedColumn; + i1.GeneratedColumn get requiresAuthentication => + columnsByName['requires_authentication']! as i1.GeneratedColumn; + i1.GeneratedColumn get stored => + columnsByName['stored']! as i1.GeneratedColumn; + i1.GeneratedColumn get isDraftMedia => + columnsByName['is_draft_media']! as i1.GeneratedColumn; + i1.GeneratedColumn get isFavorite => + columnsByName['is_favorite']! as i1.GeneratedColumn; + i1.GeneratedColumn get hasCropAnalyzed => + columnsByName['has_crop_analyzed']! as i1.GeneratedColumn; + i1.GeneratedColumn get preProgressingProcess => + columnsByName['pre_progressing_process']! as i1.GeneratedColumn; + i1.GeneratedColumn get reuploadRequestedBy => + columnsByName['reupload_requested_by']! as i1.GeneratedColumn; + i1.GeneratedColumn get displayLimitInMilliseconds => + columnsByName['display_limit_in_milliseconds']! + as i1.GeneratedColumn; + i1.GeneratedColumn get removeAudio => + columnsByName['remove_audio']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadToken => + columnsByName['download_token']! as i1.GeneratedColumn; + i1.GeneratedColumn get encryptionKey => + columnsByName['encryption_key']! as i1.GeneratedColumn; + i1.GeneratedColumn get encryptionMac => + columnsByName['encryption_mac']! as i1.GeneratedColumn; + i1.GeneratedColumn get encryptionNonce => + columnsByName['encryption_nonce']! as i1.GeneratedColumn; + i1.GeneratedColumn get storedFileHash => + columnsByName['stored_file_hash']! as i1.GeneratedColumn; + i1.GeneratedColumn get hasThumbnail => + columnsByName['has_thumbnail']! as i1.GeneratedColumn; + i1.GeneratedColumn get sizeInBytes => + columnsByName['size_in_bytes']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAtMonth => + columnsByName['created_at_month']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_255(String aliasedName) => + i1.GeneratedColumn( + 'cloud_state', + aliasedName, + false, + type: i1.DriftSqlType.string, + $customConstraints: 'NOT NULL DEFAULT \'none\'', + defaultValue: const i1.CustomExpression('\'none\''), + ); +i1.GeneratedColumn _column_256(String aliasedName) => + i1.GeneratedColumn( + 'blurhash', + aliasedName, + true, + type: i1.DriftSqlType.string, + $customConstraints: 'NULL', + ); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -11622,6 +12155,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema20 schema) from19To20, required Future Function(i1.Migrator m, Schema21 schema) from20To21, required Future Function(i1.Migrator m, Schema22 schema) from21To22, + required Future Function(i1.Migrator m, Schema23 schema) from22To23, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -11730,6 +12264,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from21To22(migrator, schema); return 22; + case 22: + final schema = Schema23(database: database); + final migrator = i1.Migrator(database, schema); + await from22To23(migrator, schema); + return 23; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -11758,6 +12297,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema20 schema) from19To20, required Future Function(i1.Migrator m, Schema21 schema) from20To21, required Future Function(i1.Migrator m, Schema22 schema) from21To22, + required Future Function(i1.Migrator m, Schema23 schema) from22To23, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -11781,5 +12321,6 @@ i1.OnUpgrade stepByStep({ from19To20: from19To20, from20To21: from20To21, from21To22: from21To22, + from22To23: from22To23, ), ); diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index c384fff3..cde12c55 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1265,13 +1265,13 @@ abstract class AppLocalizations { /// No description provided for @proFeature3. /// /// In en, this message translates to: - /// **'✓ Restore flames'** + /// **'✓ 25GB Memories storage'** String get proFeature3; /// No description provided for @proFeature4. /// /// In en, this message translates to: - /// **'✓ Support twonly'** + /// **'✓ Restore flames'** String get proFeature4; /// No description provided for @familyFeature1. @@ -1289,7 +1289,7 @@ abstract class AppLocalizations { /// No description provided for @familyFeature3. /// /// In en, this message translates to: - /// **'✓ Restore flames'** + /// **'✓ 50GB Memories storage'** String get familyFeature3; /// No description provided for @familyFeature4. diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 062d9b29..31e7cf75 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -652,10 +652,10 @@ class AppLocalizationsDe extends AppLocalizations { String get proFeature2 => '✓ 1 zusätzlicher Plus Benutzer'; @override - String get proFeature3 => '✓ Flammen wiederherstellen'; + String get proFeature3 => '✓ 25GB Memories Speicher'; @override - String get proFeature4 => '✓ twonly unterstützen'; + String get proFeature4 => '✓ Flammen wiederherstellen'; @override String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; @@ -664,10 +664,10 @@ class AppLocalizationsDe extends AppLocalizations { String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer'; @override - String get familyFeature3 => '✓ Flammen wiederherstellen'; + String get familyFeature3 => '50GB Memories Speicher'; @override - String get familyFeature4 => '✓ twonly unterstützen'; + String get familyFeature4 => '✓ Flammen wiederherstellen'; @override String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index c84e928f..550684a3 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -647,10 +647,10 @@ class AppLocalizationsEn extends AppLocalizations { String get proFeature2 => '✓ 1 additional Plus user'; @override - String get proFeature3 => '✓ Restore flames'; + String get proFeature3 => '✓ 25GB Memories storage'; @override - String get proFeature4 => '✓ Support twonly'; + String get proFeature4 => '✓ Restore flames'; @override String get familyFeature1 => '✓ Unlimited media file uploads'; @@ -659,7 +659,7 @@ class AppLocalizationsEn extends AppLocalizations { String get familyFeature2 => '✓ 4 additional Plus user'; @override - String get familyFeature3 => '✓ Restore flames'; + String get familyFeature3 => '✓ 50GB Memories storage'; @override String get familyFeature4 => '✓ Support twonly'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index ef0aed22..499ad4a7 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit ef0aed22f39a7453a9dbdbe26a098159b792dbac +Subproject commit 499ad4a7b703c1dff414f531c528894ad0c91799 diff --git a/lib/src/model/json/onboarding_state.model.g.dart b/lib/src/model/json/onboarding_state.model.g.dart index 7987797a..f02468f3 100644 --- a/lib/src/model/json/onboarding_state.model.g.dart +++ b/lib/src/model/json/onboarding_state.model.g.dart @@ -11,9 +11,6 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson( ) => ReceivedRecoveryShare( messageId: (json['messageId'] as num).toInt(), trustedFriendDisplayName: json['trustedFriendDisplayName'] as String, - trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List?) - ?.map((e) => (e as num).toInt()) - .toList(), myDisplayName: json['myDisplayName'] as String, myUserId: (json['myUserId'] as num).toInt(), myAvatarSvg: (json['myAvatarSvg'] as List?) @@ -23,6 +20,9 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson( sharedSecretDataBytes: (json['sharedSecretDataBytes'] as List) .map((e) => (e as num).toInt()) .toList(), + trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), ); Map _$ReceivedRecoveryShareToJson( diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 10118bcd..1a05106c 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -150,8 +150,8 @@ class UserData { @Deprecated('Use the secure storage in rust') TwonlySafeBackup? twonlySafeBackup; - @JsonKey(defaultValue: false) - bool isBackupEnabled = false; + @JsonKey(defaultValue: true) + bool isBackupEnabled = true; PasswordLessRecovery? passwordLessRecovery; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index b8105cec..b560d6d4 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -96,7 +96,7 @@ UserData _$UserDataFromJson(Map json) => : TwonlySafeBackup.fromJson( json['twonlySafeBackup'] as Map, ) - ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false + ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? true ..passwordLessRecovery = json['passwordLessRecovery'] == null ? null : PasswordLessRecovery.fromJson( diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index b5ad1689..d9d4e85c 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -2671,10 +2671,12 @@ class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage { factory ApplicationData_RequestMemoriesUpload({ $fixnum.Int64? size, $fixnum.Int64? originalDate, + $core.String? mediaId, }) { final result = create(); if (size != null) result.size = size; if (originalDate != null) result.originalDate = originalDate; + if (mediaId != null) result.mediaId = mediaId; return result; } @@ -2695,6 +2697,7 @@ class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage { createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'size') ..aInt64(2, _omitFieldNames ? '' : 'originalDate') + ..aOS(3, _omitFieldNames ? '' : 'mediaId') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2737,6 +2740,15 @@ class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage { $core.bool hasOriginalDate() => $_has(1); @$pb.TagNumber(2) void clearOriginalDate() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get mediaId => $_getSZ(2); + @$pb.TagNumber(3) + set mediaId($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasMediaId() => $_has(2); + @$pb.TagNumber(3) + void clearMediaId() => $_clearField(3); } class ApplicationData_ConfirmMemoriesUpload extends $pb.GeneratedMessage { @@ -2873,9 +2885,11 @@ class ApplicationData_GetMemoriesList extends $pb.GeneratedMessage { class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage { factory ApplicationData_GetMemoriesUrl({ $core.String? mediaId, + $core.bool? thumbnail, }) { final result = create(); if (mediaId != null) result.mediaId = mediaId; + if (thumbnail != null) result.thumbnail = thumbnail; return result; } @@ -2894,6 +2908,7 @@ class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage { const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'mediaId') + ..aOB(2, _omitFieldNames ? '' : 'thumbnail') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2926,6 +2941,15 @@ class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage { $core.bool hasMediaId() => $_has(0); @$pb.TagNumber(1) void clearMediaId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.bool get thumbnail => $_getBF(1); + @$pb.TagNumber(2) + set thumbnail($core.bool value) => $_setBool(1, value); + @$pb.TagNumber(2) + $core.bool hasThumbnail() => $_has(1); + @$pb.TagNumber(2) + void clearThumbnail() => $_clearField(2); } class ApplicationData_GetMemoriesUsage extends $pb.GeneratedMessage { @@ -2972,6 +2996,64 @@ class ApplicationData_GetMemoriesUsage extends $pb.GeneratedMessage { static ApplicationData_GetMemoriesUsage? _defaultInstance; } +class ApplicationData_DeleteMemory extends $pb.GeneratedMessage { + factory ApplicationData_DeleteMemory({ + $core.String? mediaId, + }) { + final result = create(); + if (mediaId != null) result.mediaId = mediaId; + return result; + } + + ApplicationData_DeleteMemory._(); + + factory ApplicationData_DeleteMemory.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_DeleteMemory.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.DeleteMemory', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'mediaId') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_DeleteMemory clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_DeleteMemory copyWith( + void Function(ApplicationData_DeleteMemory) updates) => + super.copyWith( + (message) => updates(message as ApplicationData_DeleteMemory)) + as ApplicationData_DeleteMemory; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_DeleteMemory create() => + ApplicationData_DeleteMemory._(); + @$core.override + ApplicationData_DeleteMemory createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_DeleteMemory getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_DeleteMemory? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get mediaId => $_getSZ(0); + @$pb.TagNumber(1) + set mediaId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasMediaId() => $_has(0); + @$pb.TagNumber(1) + void clearMediaId() => $_clearField(1); +} + enum ApplicationData_ApplicationData { textMessage, getUserByUsername, @@ -3006,6 +3088,7 @@ enum ApplicationData_ApplicationData { getMemoriesList, getMemoriesUrl, getMemoriesUsage, + deleteMemory, notSet } @@ -3044,6 +3127,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetMemoriesList? getMemoriesList, ApplicationData_GetMemoriesUrl? getMemoriesUrl, ApplicationData_GetMemoriesUsage? getMemoriesUsage, + ApplicationData_DeleteMemory? deleteMemory, }) { final result = create(); if (textMessage != null) result.textMessage = textMessage; @@ -3089,6 +3173,7 @@ class ApplicationData extends $pb.GeneratedMessage { if (getMemoriesList != null) result.getMemoriesList = getMemoriesList; if (getMemoriesUrl != null) result.getMemoriesUrl = getMemoriesUrl; if (getMemoriesUsage != null) result.getMemoriesUsage = getMemoriesUsage; + if (deleteMemory != null) result.deleteMemory = deleteMemory; return result; } @@ -3136,6 +3221,7 @@ class ApplicationData extends $pb.GeneratedMessage { 35: ApplicationData_ApplicationData.getMemoriesList, 36: ApplicationData_ApplicationData.getMemoriesUrl, 37: ApplicationData_ApplicationData.getMemoriesUsage, + 38: ApplicationData_ApplicationData.deleteMemory, 0: ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -3176,7 +3262,8 @@ class ApplicationData extends $pb.GeneratedMessage { 34, 35, 36, - 37 + 37, + 38 ]) ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', @@ -3289,6 +3376,9 @@ class ApplicationData extends $pb.GeneratedMessage { ..aOM( 37, _omitFieldNames ? '' : 'getMemoriesUsage', subBuilder: ApplicationData_GetMemoriesUsage.create) + ..aOM( + 38, _omitFieldNames ? '' : 'deleteMemory', + subBuilder: ApplicationData_DeleteMemory.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -3343,6 +3433,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(35) @$pb.TagNumber(36) @$pb.TagNumber(37) + @$pb.TagNumber(38) ApplicationData_ApplicationData whichApplicationData() => _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -3378,6 +3469,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(35) @$pb.TagNumber(36) @$pb.TagNumber(37) + @$pb.TagNumber(38) void clearApplicationData() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -3775,6 +3867,17 @@ class ApplicationData extends $pb.GeneratedMessage { void clearGetMemoriesUsage() => $_clearField(37); @$pb.TagNumber(37) ApplicationData_GetMemoriesUsage ensureGetMemoriesUsage() => $_ensure(32); + + @$pb.TagNumber(38) + ApplicationData_DeleteMemory get deleteMemory => $_getN(33); + @$pb.TagNumber(38) + set deleteMemory(ApplicationData_DeleteMemory value) => $_setField(38, value); + @$pb.TagNumber(38) + $core.bool hasDeleteMemory() => $_has(33); + @$pb.TagNumber(38) + void clearDeleteMemory() => $_clearField(38); + @$pb.TagNumber(38) + ApplicationData_DeleteMemory ensureDeleteMemory() => $_ensure(33); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index 92051db5..f9fa85c5 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -778,6 +778,15 @@ const ApplicationData$json = { '9': 0, '10': 'getMemoriesUsage' }, + { + '1': 'delete_memory', + '3': 38, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.DeleteMemory', + '9': 0, + '10': 'deleteMemory' + }, ], '3': [ ApplicationData_TextMessage$json, @@ -805,7 +814,8 @@ const ApplicationData$json = { ApplicationData_ConfirmMemoriesUpload$json, ApplicationData_GetMemoriesList$json, ApplicationData_GetMemoriesUrl$json, - ApplicationData_GetMemoriesUsage$json + ApplicationData_GetMemoriesUsage$json, + ApplicationData_DeleteMemory$json ], '8': [ {'1': 'ApplicationData'}, @@ -1026,6 +1036,7 @@ const ApplicationData_RequestMemoriesUpload$json = { '2': [ {'1': 'size', '3': 1, '4': 1, '5': 3, '10': 'size'}, {'1': 'original_date', '3': 2, '4': 1, '5': 3, '10': 'originalDate'}, + {'1': 'media_id', '3': 3, '4': 1, '5': 9, '10': 'mediaId'}, ], }; @@ -1051,6 +1062,7 @@ const ApplicationData_GetMemoriesUrl$json = { '1': 'GetMemoriesUrl', '2': [ {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, + {'1': 'thumbnail', '3': 2, '4': 1, '5': 8, '10': 'thumbnail'}, ], }; @@ -1059,6 +1071,14 @@ const ApplicationData_GetMemoriesUsage$json = { '1': 'GetMemoriesUsage', }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_DeleteMemory$json = { + '1': 'DeleteMemory', + '2': [ + {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, + ], +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' @@ -1118,36 +1138,39 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'AFIPZ2V0TWVtb3JpZXNMaXN0ElwKEGdldF9tZW1vcmllc191cmwYJCABKAsyMC5jbGllbnRfdG' '9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc1VybEgAUg5nZXRNZW1vcmllc1Vy' 'bBJiChJnZXRfbWVtb3JpZXNfdXNhZ2UYJSABKAsyMi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2' - 'F0aW9uRGF0YS5HZXRNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2UaagoLVGV4dE1l' - 'c3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZHkSIAoJcH' - 'VzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0VXNlckJ5' - 'VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEh' - 'oKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpn' - 'b29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIA' - 'EoA1IGdXNlcklkGhMKEUdldEF2YWlsYWJsZVBsYW5zGhUKE0dldEN1cnJlbnRQbGFuSW5mb3Ma' - 'LwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldF' - 'ByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJl' - 'S2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcm' - 'VLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVk' - 'X3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGA' - 'MgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2Fk' - 'X3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdX' - 'Nlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoL' - 'SVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIA' - 'EoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRh' - 'Gg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlch' - 'IXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tl' - 'bhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkGo4BChxSZWdpc3RlclBhc3N3b3JkTG' - 'Vzc1JlY292ZXJ5Ei4KEmVuY3J5cHRlZFNlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVy' - 'S2V5EisKDnBpblVubG9ja1Rva2VuGAIgASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW' - '5VbmxvY2tUb2tlbhpwChhQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9u' - 'X2lkGAEgASgJUg5ub3RpZmljYXRpb25JZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW' - '5jcnlwdGVkTWVzc2FnZRpQChVSZXF1ZXN0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IE' - 'c2l6ZRIjCg1vcmlnaW5hbF9kYXRlGAIgASgDUgxvcmlnaW5hbERhdGUaMgoVQ29uZmlybU1lbW' - '9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dldE1lbW9yaWVzTGlz' - 'dBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdBgCIAEoA1IFbGltaX' - 'QaKwoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQaEgoQR2V0TWVt' - 'b3JpZXNVc2FnZUIRCg9BcHBsaWNhdGlvbkRhdGE='); + 'F0aW9uRGF0YS5HZXRNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2USVQoNZGVsZXRl' + 'X21lbW9yeRgmIAEoCzIuLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlbGV0ZU' + '1lbW9yeUgAUgxkZWxldGVNZW1vcnkaagoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IG' + 'dXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZHkSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYX' + 'RhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASAB' + 'KAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2Vybm' + 'FtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVG' + 'Y20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGhMKEUdldEF2YWlsYW' + 'JsZVBsYW5zGhUKE0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXIS' + 'FwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2' + 'lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgB' + 'IAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZB' + 'gBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJl' + 'a2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYX' + 'R1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRv' + 'a2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2' + 'VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9p' + 'ZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdG' + 'lvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVs' + 'ZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySW' + 'QaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpE' + 'ZXByZWNhdGVkGo4BChxSZWdpc3RlclBhc3N3b3JkTGVzc1JlY292ZXJ5Ei4KEmVuY3J5cHRlZF' + 'NlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVyS2V5EisKDnBpblVubG9ja1Rva2VuGAIg' + 'ASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW5VbmxvY2tUb2tlbhpwChhQYXNzd29yZG' + 'xlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb25J' + 'ZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW5jcnlwdGVkTWVzc2FnZRprChVSZXF1ZX' + 'N0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IEc2l6ZRIjCg1vcmlnaW5hbF9kYXRlGAIg' + 'ASgDUgxvcmlnaW5hbERhdGUSGQoIbWVkaWFfaWQYAyABKAlSB21lZGlhSWQaMgoVQ29uZmlybU' + '1lbW9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dldE1lbW9yaWVz' + 'TGlzdBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdBgCIAEoA1IFbG' + 'ltaXQaSQoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSHAoJdGh1' + 'bWJuYWlsGAIgASgIUgl0aHVtYm5haWwaEgoQR2V0TWVtb3JpZXNVc2FnZRopCgxEZWxldGVNZW' + '1vcnkSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWRCEQoPQXBwbGljYXRpb25EYXRh'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 49f1bd7c..134ca666 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -1647,17 +1647,82 @@ class Response_PasswordlessNotificationMessages extends $pb.GeneratedMessage { $_getList(0); } +class Response_PresignedPost extends $pb.GeneratedMessage { + factory Response_PresignedPost({ + $core.String? url, + $core.Iterable<$core.MapEntry<$core.String, $core.String>>? fields, + }) { + final result = create(); + if (url != null) result.url = url; + if (fields != null) result.fields.addEntries(fields); + return result; + } + + Response_PresignedPost._(); + + factory Response_PresignedPost.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_PresignedPost.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.PresignedPost', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'url') + ..m<$core.String, $core.String>(2, _omitFieldNames ? '' : 'fields', + entryClassName: 'Response.PresignedPost.FieldsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('server_to_client')) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_PresignedPost clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_PresignedPost copyWith( + void Function(Response_PresignedPost) updates) => + super.copyWith((message) => updates(message as Response_PresignedPost)) + as Response_PresignedPost; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_PresignedPost create() => Response_PresignedPost._(); + @$core.override + Response_PresignedPost createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_PresignedPost getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Response_PresignedPost? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get url => $_getSZ(0); + @$pb.TagNumber(1) + set url($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasUrl() => $_has(0); + @$pb.TagNumber(1) + void clearUrl() => $_clearField(1); + + @$pb.TagNumber(2) + $pb.PbMap<$core.String, $core.String> get fields => $_getMap(1); +} + class Response_MemoriesUploadUrls extends $pb.GeneratedMessage { factory Response_MemoriesUploadUrls({ $core.String? mediaId, - $core.String? thumbnailUploadUrl, - $core.String? fullUploadUrl, + Response_PresignedPost? thumbnailUpload, + Response_PresignedPost? fullUpload, }) { final result = create(); if (mediaId != null) result.mediaId = mediaId; - if (thumbnailUploadUrl != null) - result.thumbnailUploadUrl = thumbnailUploadUrl; - if (fullUploadUrl != null) result.fullUploadUrl = fullUploadUrl; + if (thumbnailUpload != null) result.thumbnailUpload = thumbnailUpload; + if (fullUpload != null) result.fullUpload = fullUpload; return result; } @@ -1676,8 +1741,10 @@ class Response_MemoriesUploadUrls extends $pb.GeneratedMessage { const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'mediaId') - ..aOS(2, _omitFieldNames ? '' : 'thumbnailUploadUrl') - ..aOS(3, _omitFieldNames ? '' : 'fullUploadUrl') + ..aOM(2, _omitFieldNames ? '' : 'thumbnailUpload', + subBuilder: Response_PresignedPost.create) + ..aOM(3, _omitFieldNames ? '' : 'fullUpload', + subBuilder: Response_PresignedPost.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1712,22 +1779,26 @@ class Response_MemoriesUploadUrls extends $pb.GeneratedMessage { void clearMediaId() => $_clearField(1); @$pb.TagNumber(2) - $core.String get thumbnailUploadUrl => $_getSZ(1); + Response_PresignedPost get thumbnailUpload => $_getN(1); @$pb.TagNumber(2) - set thumbnailUploadUrl($core.String value) => $_setString(1, value); + set thumbnailUpload(Response_PresignedPost value) => $_setField(2, value); @$pb.TagNumber(2) - $core.bool hasThumbnailUploadUrl() => $_has(1); + $core.bool hasThumbnailUpload() => $_has(1); @$pb.TagNumber(2) - void clearThumbnailUploadUrl() => $_clearField(2); + void clearThumbnailUpload() => $_clearField(2); + @$pb.TagNumber(2) + Response_PresignedPost ensureThumbnailUpload() => $_ensure(1); @$pb.TagNumber(3) - $core.String get fullUploadUrl => $_getSZ(2); + Response_PresignedPost get fullUpload => $_getN(2); @$pb.TagNumber(3) - set fullUploadUrl($core.String value) => $_setString(2, value); + set fullUpload(Response_PresignedPost value) => $_setField(3, value); @$pb.TagNumber(3) - $core.bool hasFullUploadUrl() => $_has(2); + $core.bool hasFullUpload() => $_has(2); @$pb.TagNumber(3) - void clearFullUploadUrl() => $_clearField(3); + void clearFullUpload() => $_clearField(3); + @$pb.TagNumber(3) + Response_PresignedPost ensureFullUpload() => $_ensure(2); } class Response_MediaItem extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index dd2041f9..d918a21a 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -178,6 +178,7 @@ const Response$json = { Response_ProofOfWork$json, Response_PasswordlessNotificationMessage$json, Response_PasswordlessNotificationMessages$json, + Response_PresignedPost$json, Response_MemoriesUploadUrls$json, Response_MediaItem$json, Response_MemoriesList$json, @@ -553,19 +554,54 @@ const Response_PasswordlessNotificationMessages$json = { ], }; +@$core.Deprecated('Use responseDescriptor instead') +const Response_PresignedPost$json = { + '1': 'PresignedPost', + '2': [ + {'1': 'url', '3': 1, '4': 1, '5': 9, '10': 'url'}, + { + '1': 'fields', + '3': 2, + '4': 3, + '5': 11, + '6': '.server_to_client.Response.PresignedPost.FieldsEntry', + '10': 'fields' + }, + ], + '3': [Response_PresignedPost_FieldsEntry$json], +}; + +@$core.Deprecated('Use responseDescriptor instead') +const Response_PresignedPost_FieldsEntry$json = { + '1': 'FieldsEntry', + '2': [ + {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': {'7': true}, +}; + @$core.Deprecated('Use responseDescriptor instead') const Response_MemoriesUploadUrls$json = { '1': 'MemoriesUploadUrls', '2': [ {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, { - '1': 'thumbnail_upload_url', + '1': 'thumbnail_upload', '3': 2, '4': 1, - '5': 9, - '10': 'thumbnailUploadUrl' + '5': 11, + '6': '.server_to_client.Response.PresignedPost', + '10': 'thumbnailUpload' + }, + { + '1': 'full_upload', + '3': 3, + '4': 1, + '5': 11, + '6': '.server_to_client.Response.PresignedPost', + '10': 'fullUpload' }, - {'1': 'full_upload_url', '3': 3, '4': 1, '5': 9, '10': 'fullUploadUrl'}, ], }; @@ -843,42 +879,46 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEisKEWVuY3J5' 'cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3JkbGVzc05vdG' 'lmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2NsaWVudC5S' - 'ZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdlcxqJAQoSTW' - 'Vtb3JpZXNVcGxvYWRVcmxzEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkEjAKFHRodW1ibmFp' - 'bF91cGxvYWRfdXJsGAIgASgJUhJ0aHVtYm5haWxVcGxvYWRVcmwSJgoPZnVsbF91cGxvYWRfdX' - 'JsGAMgASgJUg1mdWxsVXBsb2FkVXJsGoEBCglNZWRpYUl0ZW0SGQoIbWVkaWFfaWQYASABKAlS' - 'B21lZGlhSWQSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3JpZ2luYWxEYXRlEjQKFnRodW1ibm' - 'FpbF9kb3dubG9hZF91cmwYAyABKAlSFHRodW1ibmFpbERvd25sb2FkVXJsGkoKDE1lbW9yaWVz' - 'TGlzdBI6CgVpdGVtcxgBIAMoCzIkLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVkaWFJdG' - 'VtUgVpdGVtcxo5CgtNZW1vcmllc1VybBIqChFmdWxsX2Rvd25sb2FkX3VybBgBIAEoCVIPZnVs' - 'bERvd25sb2FkVXJsGmcKDU1lbW9yaWVzVXNhZ2USGwoJbWF4X2J5dGVzGAEgASgDUghtYXhCeX' - 'RlcxIjCg1jdXJyZW50X2J5dGVzGAIgASgDUgxjdXJyZW50Qnl0ZXMSFAoFY291bnQYAyABKANS' - 'BWNvdW50GoMMCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBn' - 'VzZXJpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2Fk' - 'dG9rZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3' - 'VwbG9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z' - 'ZS5Vc2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEk' - 'oKDGRlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVj' - 'YXRlZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2' - 'NsaWVudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMY' - 'CSABKAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW' - '5iYWxsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNl' - 'SABSDHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaW' - 'VudC5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2lu' - 'dml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aX' - 'Rlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2' - 'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk' - '0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVk' - 'UHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b1' - '9jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmsSSQogcGFzc3dvcmRs' - 'ZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlTZX' - 'J2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRpb25fbWVzc2FnZXMYESABKAsyOy5z' - 'ZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2' - 'VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzEmEKFG1lbW9yaWVzX3VwbG9h' - 'ZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5NZW1vcmllc1VwbG9hZF' - 'VybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9yaWVzX2xpc3QYEyABKAsyJy5zZXJ2' - 'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdEgAUgxtZW1vcmllc0xpc3QSSwoMbW' - 'Vtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5NZW1vcmllc1Vy' - 'bEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZRgVIAEoCzIoLnNlcnZlcl90b19jbG' - 'llbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1vcmllc1VzYWdlQgQKAk9rQgoKCFJl' - 'c3BvbnNl'); + 'ZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdlcxqqAQoNUH' + 'Jlc2lnbmVkUG9zdBIQCgN1cmwYASABKAlSA3VybBJMCgZmaWVsZHMYAiADKAsyNC5zZXJ2ZXJf' + 'dG9fY2xpZW50LlJlc3BvbnNlLlByZXNpZ25lZFBvc3QuRmllbGRzRW50cnlSBmZpZWxkcxo5Cg' + 'tGaWVsZHNFbnRyeRIQCgNrZXkYASABKAlSA2tleRIUCgV2YWx1ZRgCIAEoCVIFdmFsdWU6AjgB' + 'Gs8BChJNZW1vcmllc1VwbG9hZFVybHMSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSUwoQdG' + 'h1bWJuYWlsX3VwbG9hZBgCIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlc2ln' + 'bmVkUG9zdFIPdGh1bWJuYWlsVXBsb2FkEkkKC2Z1bGxfdXBsb2FkGAMgASgLMiguc2VydmVyX3' + 'RvX2NsaWVudC5SZXNwb25zZS5QcmVzaWduZWRQb3N0UgpmdWxsVXBsb2FkGoEBCglNZWRpYUl0' + 'ZW0SGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3' + 'JpZ2luYWxEYXRlEjQKFnRodW1ibmFpbF9kb3dubG9hZF91cmwYAyABKAlSFHRodW1ibmFpbERv' + 'd25sb2FkVXJsGkoKDE1lbW9yaWVzTGlzdBI6CgVpdGVtcxgBIAMoCzIkLnNlcnZlcl90b19jbG' + 'llbnQuUmVzcG9uc2UuTWVkaWFJdGVtUgVpdGVtcxo5CgtNZW1vcmllc1VybBIqChFmdWxsX2Rv' + 'd25sb2FkX3VybBgBIAEoCVIPZnVsbERvd25sb2FkVXJsGmcKDU1lbW9yaWVzVXNhZ2USGwoJbW' + 'F4X2J5dGVzGAEgASgDUghtYXhCeXRlcxIjCg1jdXJyZW50X2J5dGVzGAIgASgDUgxjdXJyZW50' + 'Qnl0ZXMSFAoFY291bnQYAyABKANSBWNvdW50GoMMCgJPaxIUCgROb25lGAEgASgISABSBE5vbm' + 'USGAoGdXNlcmlkGAIgASgDSABSBnVzZXJpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1' + 'dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9rZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3' + 'BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2Vy' + 'dmVyX3RvX2NsaWVudC5SZXNwb25zZS5Vc2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW' + '4YBiABKAxIAFIJYXV0aHRva2VuEkoKDGRlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19j' + 'bGllbnQuUmVzcG9uc2UuRGVwcmVjYXRlZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdG' + 'VkGAggASgLMiguc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1' + 'dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSABKAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLl' + 'BsYW5zSABSBXBsYW5zEk0KDHBsYW5iYWxsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQu' + 'UmVzcG9uc2UuUGxhbkJhbGxhbmNlSABSDHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGA' + 'sgASgLMiUuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0' + 'ZWQxMRJfChJhZGRhY2NvdW50c2ludml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3' + 'BvbnNlLkFkZEFjY291bnRzSW52aXRlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxv' + 'YWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW' + '5zSABSDmRvd25sb2FkdG9rZW5zEk0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19j' + 'bGllbnQuUmVzcG9uc2UuU2lnbmVkUHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV2' + '9yaxgPIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJv' + 'b2ZPZldvcmsSSQogcGFzc3dvcmRsZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcG' + 'Fzc3dvcmRsZXNzUmVjb3ZlcnlTZXJ2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRp' + 'b25fbWVzc2FnZXMYESABKAsyOy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbG' + 'Vzc05vdGlmaWNhdGlvbk1lc3NhZ2VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3Nh' + 'Z2VzEmEKFG1lbW9yaWVzX3VwbG9hZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZX' + 'Nwb25zZS5NZW1vcmllc1VwbG9hZFVybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9y' + 'aWVzX2xpc3QYEyABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdE' + 'gAUgxtZW1vcmllc0xpc3QSSwoMbWVtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVu' + 'dC5SZXNwb25zZS5NZW1vcmllc1VybEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZR' + 'gVIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1v' + 'cmllc1VzYWdlQgQKAk9rQgoKCFJlc3BvbnNl'); diff --git a/lib/src/model/protobuf/client/backup.proto b/lib/src/model/protobuf/client/backup.proto index e3fa952c..c9f7910d 100644 --- a/lib/src/model/protobuf/client/backup.proto +++ b/lib/src/model/protobuf/client/backup.proto @@ -9,4 +9,11 @@ message TwonlySafeBackupEncrypted { bytes mac = 1; bytes nonce = 2; bytes cipher_text = 3; +} + +message CloudMediaBackupEncrypted { + string addition = 1; + bytes encrypted_media_key = 2; // Generated by Rust MainKey + bytes media_nonce = 3; // Nonce used for ChaCha20 + bytes media_ciphertext = 4; // The actual media_ciphertext (including the Poly1305 MAC) } \ No newline at end of file diff --git a/lib/src/model/protobuf/client/generated/backup.pb.dart b/lib/src/model/protobuf/client/generated/backup.pb.dart index 576ea5c1..d746575d 100644 --- a/lib/src/model/protobuf/client/generated/backup.pb.dart +++ b/lib/src/model/protobuf/client/generated/backup.pb.dart @@ -164,6 +164,99 @@ class TwonlySafeBackupEncrypted extends $pb.GeneratedMessage { void clearCipherText() => $_clearField(3); } +class CloudMediaBackupEncrypted extends $pb.GeneratedMessage { + factory CloudMediaBackupEncrypted({ + $core.String? addition, + $core.List<$core.int>? encryptedMediaKey, + $core.List<$core.int>? mediaNonce, + $core.List<$core.int>? mediaCiphertext, + }) { + final result = create(); + if (addition != null) result.addition = addition; + if (encryptedMediaKey != null) result.encryptedMediaKey = encryptedMediaKey; + if (mediaNonce != null) result.mediaNonce = mediaNonce; + if (mediaCiphertext != null) result.mediaCiphertext = mediaCiphertext; + return result; + } + + CloudMediaBackupEncrypted._(); + + factory CloudMediaBackupEncrypted.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory CloudMediaBackupEncrypted.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'CloudMediaBackupEncrypted', + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'addition') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'encryptedMediaKey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 3, _omitFieldNames ? '' : 'mediaNonce', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'mediaCiphertext', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CloudMediaBackupEncrypted clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + CloudMediaBackupEncrypted copyWith( + void Function(CloudMediaBackupEncrypted) updates) => + super.copyWith((message) => updates(message as CloudMediaBackupEncrypted)) + as CloudMediaBackupEncrypted; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CloudMediaBackupEncrypted create() => CloudMediaBackupEncrypted._(); + @$core.override + CloudMediaBackupEncrypted createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static CloudMediaBackupEncrypted getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static CloudMediaBackupEncrypted? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get addition => $_getSZ(0); + @$pb.TagNumber(1) + set addition($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasAddition() => $_has(0); + @$pb.TagNumber(1) + void clearAddition() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get encryptedMediaKey => $_getN(1); + @$pb.TagNumber(2) + set encryptedMediaKey($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasEncryptedMediaKey() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedMediaKey() => $_clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get mediaNonce => $_getN(2); + @$pb.TagNumber(3) + set mediaNonce($core.List<$core.int> value) => $_setBytes(2, value); + @$pb.TagNumber(3) + $core.bool hasMediaNonce() => $_has(2); + @$pb.TagNumber(3) + void clearMediaNonce() => $_clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get mediaCiphertext => $_getN(3); + @$pb.TagNumber(4) + set mediaCiphertext($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasMediaCiphertext() => $_has(3); + @$pb.TagNumber(4) + void clearMediaCiphertext() => $_clearField(4); +} + const $core.bool _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const $core.bool _omitMessageNames = diff --git a/lib/src/model/protobuf/client/generated/backup.pbjson.dart b/lib/src/model/protobuf/client/generated/backup.pbjson.dart index ad11d857..761a1cb0 100644 --- a/lib/src/model/protobuf/client/generated/backup.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/backup.pbjson.dart @@ -51,3 +51,27 @@ final $typed_data.Uint8List twonlySafeBackupEncryptedDescriptor = $convert.base64Decode( 'ChlUd29ubHlTYWZlQmFja3VwRW5jcnlwdGVkEhAKA21hYxgBIAEoDFIDbWFjEhQKBW5vbmNlGA' 'IgASgMUgVub25jZRIfCgtjaXBoZXJfdGV4dBgDIAEoDFIKY2lwaGVyVGV4dA=='); + +@$core.Deprecated('Use cloudMediaBackupEncryptedDescriptor instead') +const CloudMediaBackupEncrypted$json = { + '1': 'CloudMediaBackupEncrypted', + '2': [ + {'1': 'addition', '3': 1, '4': 1, '5': 9, '10': 'addition'}, + { + '1': 'encrypted_media_key', + '3': 2, + '4': 1, + '5': 12, + '10': 'encryptedMediaKey' + }, + {'1': 'media_nonce', '3': 3, '4': 1, '5': 12, '10': 'mediaNonce'}, + {'1': 'media_ciphertext', '3': 4, '4': 1, '5': 12, '10': 'mediaCiphertext'}, + ], +}; + +/// Descriptor for `CloudMediaBackupEncrypted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List cloudMediaBackupEncryptedDescriptor = $convert.base64Decode( + 'ChlDbG91ZE1lZGlhQmFja3VwRW5jcnlwdGVkEhoKCGFkZGl0aW9uGAEgASgJUghhZGRpdGlvbh' + 'IuChNlbmNyeXB0ZWRfbWVkaWFfa2V5GAIgASgMUhFlbmNyeXB0ZWRNZWRpYUtleRIfCgttZWRp' + 'YV9ub25jZRgDIAEoDFIKbWVkaWFOb25jZRIpChBtZWRpYV9jaXBoZXJ0ZXh0GAQgASgMUg9tZW' + 'RpYUNpcGhlcnRleHQ='); diff --git a/lib/src/services/api/api.service.dart b/lib/src/services/api/api.service.dart index f31cf0f2..60f7e91f 100644 --- a/lib/src/services/api/api.service.dart +++ b/lib/src/services/api/api.service.dart @@ -32,6 +32,7 @@ import 'package:twonly/src/services/api/server_messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/group.service.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart'; @@ -143,6 +144,7 @@ class ApiService { unawaited(PasswordlessRecoveryService.performHeartbeat()); unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); + memoriesCloudService.init(); if (userService.currentUser.userStudyParticipantsToken != null) { // In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection @@ -710,6 +712,75 @@ class ApiService { return sendRequestSync(req); } + Future requestMemoriesUpload( + int sizeBytes, + DateTime originalDate, + String mediaId, + ) async { + final get = ApplicationData_RequestMemoriesUpload() + ..size = Int64(sizeBytes) + ..originalDate = Int64(originalDate.millisecondsSinceEpoch) + ..mediaId = mediaId; + final appData = ApplicationData()..requestMemoriesUpload = get; + final req = createClientToServerFromApplicationData(appData); + final res = await sendRequestSync(req); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasMemoriesUploadUrls()) { + return ok.memoriesUploadUrls; + } + } + return null; + } + + Future getMemoriesUsage() async { + final appData = ApplicationData() + ..getMemoriesUsage = ApplicationData_GetMemoriesUsage(); + final req = createClientToServerFromApplicationData(appData); + final res = await sendRequestSync(req); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasMemoriesUsage()) { + return ok.memoriesUsage; + } + } + return null; + } + + Future getMemoriesUrl( + String mediaId, + bool thumbnail, + ) async { + final appData = ApplicationData() + ..getMemoriesUrl = ApplicationData_GetMemoriesUrl( + mediaId: mediaId, + thumbnail: thumbnail, + ); + final req = createClientToServerFromApplicationData(appData); + final res = await sendRequestSync(req); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasMemoriesUrl()) { + return ok.memoriesUrl; + } + } + return null; + } + + Future confirmMemoriesUpload(String mediaId) async { + final get = ApplicationData_ConfirmMemoriesUpload()..mediaId = mediaId; + final appData = ApplicationData()..confirmMemoriesUpload = get; + final req = createClientToServerFromApplicationData(appData); + return sendRequestSync(req); + } + + Future deleteMemory(String mediaId) async { + final get = ApplicationData_DeleteMemory()..mediaId = mediaId; + final appData = ApplicationData()..deleteMemory = get; + final req = createClientToServerFromApplicationData(appData); + return sendRequestSync(req); + } + Future getUserIdFromUsername(String username) async { final appData = Handshake( getUseridByUsername: Handshake_GetUserIdByUsername(username: username), diff --git a/lib/src/services/api/mediafiles/media_background.api.dart b/lib/src/services/api/mediafiles/media_background.api.dart index 92afafc1..68f193f1 100644 --- a/lib/src/services/api/mediafiles/media_background.api.dart +++ b/lib/src/services/api/mediafiles/media_background.api.dart @@ -22,6 +22,7 @@ Future initFileDownloader() async { if (update.task.taskId.contains('download_')) { await handleDownloadStatusUpdate(update); } + if (update.task.taskId.contains('backup_')) { await BackupService.handleBackupStatusUpdate( update.task.taskId, diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index cbe1039e..dc089ea1 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -13,6 +13,8 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/compression.service.dart'; import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart' + show MemoriesCloudService; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -220,6 +222,7 @@ class MediaFileService { } catch (_) {} } if (mediaFile.stored && + mediaFile.cloudState == CloudState.none && mediaFile.createdAt.isBefore( clock.now().subtract(const Duration(days: 30)), )) { @@ -335,6 +338,7 @@ class MediaFileService { unawaited(createThumbnail()); await calculateAndSaveSize(); await hashMediaFile(); + await MemoriesCloudService().checkUploads(); // updateFromDb is done in hashStoredMedia() } diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart index ef733657..a27f5f5d 100644 --- a/lib/src/services/mediafiles/thumbnail.service.dart +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -101,7 +101,7 @@ Future createThumbnailsForImage( destinationFile.absolute.path, minWidth: 300, minHeight: 300, - quality: 100, + quality: 50, format: CompressFormat.webp, ); stopwatch.stop(); diff --git a/lib/src/services/memories/cloud.service.dart b/lib/src/services/memories/cloud.service.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/src/services/memories/memories.service.dart b/lib/src/services/memories/memories.service.dart index 4a532f70..868ebcde 100644 --- a/lib/src/services/memories/memories.service.dart +++ b/lib/src/services/memories/memories.service.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:collection'; +import 'package:blurhash_dart/blurhash_dart.dart' as bh; import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; +import 'package:image/image.dart' as img; import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -190,7 +192,8 @@ class MemoriesService { return MemoriesState( filesToMigrate: filesToMigrate, - totalFilesToMigrate: filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating. + totalFilesToMigrate: + filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating. galleryItems: tempGalleryItems, months: tempMonths, orderedByMonth: tempOrderedByMonth, @@ -280,6 +283,27 @@ class MemoriesService { if (mediaService.mediaFile.sizeInBytes == null) { await mediaService.calculateAndSaveSize(); } + + if (mediaService.mediaFile.blurhash == null) { + try { + final imageFile = mediaService.thumbnailPath.existsSync() + ? mediaService.thumbnailPath + : mediaService.originalPath; + if (imageFile.existsSync()) { + final bytes = await imageFile.readAsBytes(); + final image = img.decodeImage(bytes); + if (image != null) { + final blurhash = bh.BlurHash.encode(image).hash; + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion(blurhash: Value(blurhash)), + ); + } + } + } catch (e) { + Log.error('Error generating blurhash for ${mediaFile.mediaId}: $e'); + } + } } catch (e) { Log.error( 'Error in background processing of ${mediaFile.mediaId}: $e', diff --git a/lib/src/services/memories/memories_cloud.service.dart b/lib/src/services/memories/memories_cloud.service.dart new file mode 100644 index 00000000..f5ff0789 --- /dev/null +++ b/lib/src/services/memories/memories_cloud.service.dart @@ -0,0 +1,394 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' + as server; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class MemoriesBackupProgress { + MemoriesBackupProgress({ + required this.totalPending, + required this.currentUploaded, + required this.currentUploadProgress, + this.currentMediaId, + }); + + final int totalPending; + final int currentUploaded; + final double currentUploadProgress; + final String? currentMediaId; +} + +class ProgressMultipartRequest extends http.MultipartRequest { + ProgressMultipartRequest(super.method, super.url, {this.onProgress}); + + final void Function(int bytes, int totalBytes)? onProgress; + + @override + http.ByteStream finalize() { + final byteStream = super.finalize(); + if (onProgress == null) return byteStream; + + final total = contentLength; + var bytes = 0; + + final transformer = StreamTransformer, List>.fromHandlers( + handleData: (data, sink) { + bytes += data.length; + onProgress!(bytes, total); + sink.add(data); + }, + ); + + return http.ByteStream(byteStream.transform(transformer)); + } +} + +class MemoriesCloudService { + Timer? _timer; + bool _isProcessing = false; + + final _progressController = + StreamController.broadcast(); + Stream get progressStream => + _progressController.stream; + + MemoriesBackupProgress? _currentProgress; + MemoriesBackupProgress? get currentProgress => _currentProgress; + + void init() { + _timer = Timer.periodic(const Duration(minutes: 5), (_) { + checkUploads(); + }); + // Run immediately + Future.delayed(const Duration(seconds: 10), checkUploads); + } + + void dispose() { + _timer?.cancel(); + _progressController.close(); + } + + void _updateProgress({ + required int totalPending, + required int currentUploaded, + required double currentUploadProgress, + String? currentMediaId, + }) { + _currentProgress = MemoriesBackupProgress( + totalPending: totalPending, + currentUploaded: currentUploaded, + currentUploadProgress: currentUploadProgress, + currentMediaId: currentMediaId, + ); + _progressController.add(_currentProgress!); + } + + Future checkUploads() async { + if (_isProcessing || !userService.currentUser.isBackupEnabled) return; + + try { + final memories = await twonlyDB.mediaFilesDao.getMemoriesToBackup(); + if (memories.isEmpty) { + return; + } + + _isProcessing = true; + + final total = memories.length; + var uploaded = 0; + + _updateProgress( + totalPending: total, + currentUploaded: uploaded, + currentUploadProgress: 0, + ); + + for (final mediaFile in memories) { + _updateProgress( + totalPending: total, + currentUploaded: uploaded, + currentUploadProgress: 0, + currentMediaId: mediaFile.mediaId, + ); + + final success = await _backupMemory(mediaFile, (progress) { + _updateProgress( + totalPending: total, + currentUploaded: uploaded, + currentUploadProgress: progress, + currentMediaId: mediaFile.mediaId, + ); + }); + + if (success) { + uploaded++; + } + + _updateProgress( + totalPending: total, + currentUploaded: uploaded, + currentUploadProgress: 0, + ); + } + } catch (e) { + Log.error('Error in MemoriesCloudService.checkUploads: $e'); + } finally { + _currentProgress = null; + _isProcessing = false; + } + } + + static Future downloadThumbnail(MediaFileService media) async { + final urls = await apiService.getMemoriesUrl(media.mediaFile.mediaId, true); + if (urls == null || !urls.hasFullDownloadUrl()) return false; + + try { + final response = await http.get(Uri.parse(urls.fullDownloadUrl)); + + if (response.statusCode == 200) { + return await _decryptFile(response.bodyBytes, media.thumbnailPath); + } else { + Log.warn( + 'Failed to download thumbnai statuscode ${response.statusCode}', + ); + } + } catch (e) { + Log.warn(e); + } + return false; + } + + Future _backupMemory( + MediaFile mediaFile, + void Function(double progress) onProgress, + ) async { + try { + final ms = MediaFileService(mediaFile); + + if (!ms.storedPath.existsSync()) return false; + + if (!mediaFile.hasThumbnail) { + await MediaFileService(mediaFile).createThumbnail(); + } + + final sizeBytes = ms.storedPath.lengthSync(); + final urls = await apiService.requestMemoriesUpload( + sizeBytes, + mediaFile.createdAt, + mediaFile.mediaId, + ); + + if (urls == null) { + Log.error('Could not get upload URLs for memory ${mediaFile.mediaId}'); + return false; + } + + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + cloudState: Value(CloudState.pending), + ), + ); + + final mediaKey = getRandomUint8List(32); + final encryptedMediaKey = await RustKeyManager.encryptCloudMediaKey( + mediaKey: mediaKey, + addition: 'app', + ); + + final tempDir = await getTemporaryDirectory(); + + // 1. Upload thumbnail if exists + if (ms.thumbnailPath.existsSync() && urls.hasThumbnailUpload()) { + final thumbFile = await _encryptFile( + ms.thumbnailPath, + mediaKey, + encryptedMediaKey, + tempDir, + 'thumb_${mediaFile.mediaId}', + ); + try { + await _uploadToS3(urls.thumbnailUpload, thumbFile, (_) {}); + } finally { + if (thumbFile.existsSync()) { + thumbFile.deleteSync(); + } + } + } + + // 2. Upload full media if exists + if (urls.hasFullUpload()) { + final fullFile = await _encryptFile( + ms.storedPath, + mediaKey, + encryptedMediaKey, + tempDir, + 'full_${mediaFile.mediaId}', + ); + try { + await _uploadToS3(urls.fullUpload, fullFile, onProgress); + } finally { + if (fullFile.existsSync()) { + fullFile.deleteSync(); + } + } + } + + // 3. Confirm upload + final confirmRes = await apiService.confirmMemoriesUpload( + mediaFile.mediaId, + ); + if (confirmRes.isSuccess) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + cloudState: Value(CloudState.uploaded), + ), + ); + Log.info( + 'Cloud backup complete and confirmed for ${mediaFile.mediaId}', + ); + return true; + } else { + throw Exception('Server confirmation failed: ${confirmRes.error}'); + } + } catch (e) { + Log.error('Error backing up memory ${mediaFile.mediaId}: $e'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + cloudState: Value(CloudState.none), + ), + ); + return false; + } + } + + Future _uploadToS3( + server.Response_PresignedPost presignedPost, + File file, + void Function(double progress) onProgress, + ) async { + final request = ProgressMultipartRequest( + 'POST', + Uri.parse(presignedPost.url), + onProgress: (bytes, total) { + if (total > 0) { + onProgress(bytes / total); + } + }, + ); + + request.fields.addAll(presignedPost.fields); + request.files.add( + await http.MultipartFile.fromPath( + 'file', + file.path, + filename: 'file', + ), + ); + + final streamedResponse = await request.send(); + final response = await http.Response.fromStream(streamedResponse); + if (response.statusCode != 200 && response.statusCode != 204) { + throw Exception( + 'S3 upload failed: ${response.statusCode} - ${response.body}', + ); + } + } + + Future _encryptFile( + File inputFile, + Uint8List mediaKey, + List encryptedMediaKey, + Directory tempDir, + String prefix, + ) async { + final dataToEncrypt = await inputFile.readAsBytes(); + final chacha20 = FlutterChacha20.poly1305Aead(); + final nonce = chacha20.newNonce(); + + final secretBox = await chacha20.encrypt( + dataToEncrypt, + secretKey: SecretKey(mediaKey), + nonce: nonce, + ); + + final cipherTextWithMac = Uint8List.fromList( + secretBox.cipherText + secretBox.mac.bytes, + ); + + final payload = CloudMediaBackupEncrypted() + ..addition = 'app' + ..encryptedMediaKey = encryptedMediaKey + ..mediaNonce = nonce + ..mediaCiphertext = cipherTextWithMac; + + final outFile = File('${tempDir.path}/$prefix.encrypted'); + await outFile.writeAsBytes(payload.writeToBuffer()); + return outFile; + } + + static Future _decryptFile( + Uint8List encryptedData, + File outFile, + ) async { + try { + // 2. Parse the protobuf payload FIRST to access the key and addition + final payload = CloudMediaBackupEncrypted.fromBuffer(encryptedData); + + // 3. Get the media key using the encrypted key and addition from the payload + final mediaKey = await RustKeyManager.decryptCloudMediaKey( + encryptedMediaKey: payload.encryptedMediaKey, + addition: payload.addition, + ); + + // 4. Extract the concatenated ciphertext + MAC, and the nonce + final nonce = payload.mediaNonce; + final cipherTextWithMac = payload.mediaCiphertext; + + // 5. Separate the ciphertext and the MAC (Poly1305 MAC is always 16 bytes) + const macLength = 16; + final cipherTextLength = cipherTextWithMac.length - macLength; + + final cipherText = cipherTextWithMac.sublist(0, cipherTextLength); + final macBytes = cipherTextWithMac.sublist(cipherTextLength); + + // 6. Reconstruct the SecretBox + final secretBox = SecretBox( + cipherText, + nonce: nonce, + mac: Mac(macBytes), + ); + + // 7. Decrypt the data using the newly retrieved media key + final chacha20 = FlutterChacha20.poly1305Aead(); + final decryptedBytes = await chacha20.decrypt( + secretBox, + secretKey: SecretKey(mediaKey), + ); + + await outFile.writeAsBytes(decryptedBytes); + return true; + } catch (e) { + Log.error(e); + return false; + } + } +} + +final memoriesCloudService = MemoriesCloudService(); diff --git a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart index 599bc012..dff1a207 100644 --- a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart +++ b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart @@ -1,7 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/visual/components/selectable_thumbnail.comp.dart'; import 'package:twonly/src/visual/views/memories/components/memory_transition_painter.dart'; @@ -31,8 +36,10 @@ class MemoriesThumbnailComp extends StatefulWidget { class _MemoriesThumbnailCompState extends State { ImageProvider? _imageProvider; + File? _selectedImageFile; ImageStream? _imageStream; ImageInfo? _imageInfo; + int _retries = 0; late final ImageStreamListener _listener; @override @@ -52,6 +59,7 @@ class _MemoriesThumbnailCompState extends State { setState(() { _imageProvider = null; _imageInfo = null; + _selectedImageFile = null; }); } }, @@ -60,6 +68,7 @@ class _MemoriesThumbnailCompState extends State { } void _resolveImage() { + if (_retries > 3) return; final media = widget.galleryItem.mediaService; final hasThumbnail = media.thumbnailPath.existsSync() && @@ -70,17 +79,33 @@ class _MemoriesThumbnailCompState extends State { media.mediaFile.type == MediaType.image || media.mediaFile.type == MediaType.gif; + _selectedImageFile = null; + if (hasThumbnail) { _imageProvider = FileImage(media.thumbnailPath); + _selectedImageFile = media.thumbnailPath; } else if (hasStored && isImageOrGif) { _imageProvider = FileImage(media.storedPath); + _selectedImageFile = media.storedPath; + } + + if (!hasThumbnail) { + if (hasStored) { + media.createThumbnail(); + } else { + MemoriesCloudService.downloadThumbnail(media).then((success) { + if (mounted && success) { + _resolveImage(); + } + }); + } } if (_imageProvider != null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final config = createLocalImageConfiguration(context); - _imageStream = _imageProvider!.resolve(config); + _imageStream = _imageProvider?.resolve(config); _imageStream!.addListener(_listener); }); } @@ -93,6 +118,8 @@ class _MemoriesThumbnailCompState extends State { widget.galleryItem.mediaService.mediaFile.mediaId) { _imageStream?.removeListener(_listener); _imageInfo = null; + _retries = 0; + _selectedImageFile = null; _resolveImage(); } } @@ -148,6 +175,14 @@ class _MemoriesThumbnailCompState extends State { fit: BoxFit.cover, gaplessPlayback: true, errorBuilder: (context, error, stackTrace) { + if (error.toString().contains('Invalid image data')) { + if (_selectedImageFile != null) { + _selectedImageFile?.deleteSync(); + _retries++; + _resolveImage(); + } + } + Log.warn(error); return ColoredBox( color: Colors.grey.shade200, child: const Center( @@ -159,6 +194,11 @@ class _MemoriesThumbnailCompState extends State { ); }, ) + else if (media.mediaFile.blurhash != null) + BlurHash( + hash: media.mediaFile.blurhash!, + optimizationMode: BlurHashOptimizationMode.approximation, + ) else ColoredBox( color: Colors.grey.shade200, @@ -195,6 +235,32 @@ class _MemoriesThumbnailCompState extends State { ], ), ), + if (media.mediaFile.cloudState == CloudState.pending) + const Positioned( + top: 6, + right: 6, + child: Icon( + Icons.cloud_upload_outlined, + color: Colors.white70, + size: 16, + shadows: [ + Shadow(color: Colors.black54, blurRadius: 4), + ], + ), + ) + else if (media.mediaFile.cloudState == CloudState.uploaded) + const Positioned( + top: 6, + right: 6, + child: Icon( + Icons.cloud_done_outlined, + color: Colors.white, + size: 16, + shadows: [ + Shadow(color: Colors.black54, blurRadius: 4), + ], + ), + ), ], ), ), diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 2ed4830b..b4071fc6 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -21,7 +21,8 @@ class MemoriesView extends StatefulWidget { State createState() => MemoriesViewState(); } -class MemoriesViewState extends State with AutomaticKeepAliveClientMixin { +class MemoriesViewState extends State + with AutomaticKeepAliveClientMixin { late final MemoriesService _service; final ValueNotifier _activeMediaIdNotifier = ValueNotifier(null); final ScrollController _scrollController = ScrollController(); @@ -31,11 +32,27 @@ class MemoriesViewState extends State with AutomaticKeepAliveClien bool _filterFavoritesOnly = false; bool get _selectionMode => _selectedMediaIds.isNotEmpty; + bool _isUsageLimitReached = false; + @override void initState() { super.initState(); _service = MemoriesService(); _activeMediaIdNotifier.addListener(_onActiveMediaChanged); + _checkUsage(); + } + + Future _checkUsage() async { + final usage = await apiService.getMemoriesUsage(); + if (usage != null && + usage.maxBytes > 0 && + usage.currentBytes >= usage.maxBytes) { + if (mounted) { + setState(() { + _isUsageLimitReached = true; + }); + } + } } @override @@ -255,17 +272,56 @@ class MemoriesViewState extends State with AutomaticKeepAliveClien Future _batchDelete() async { final count = _selectedMediaIds.length; - final confirmed = await showAlertDialog( - context, - context.lang.deleteImageTitle, - context.lang.deleteMemoriesBody(count), - ); - - if (!confirmed) return; - final items = _service.currentState.galleryItems; final selectedList = _selectedMediaIds.toList(); + var hasCloudBackup = false; + for (final id in selectedList) { + final item = items + .where((e) => e.mediaService.mediaFile.mediaId == id) + .firstOrNull; + if (item != null && + item.mediaService.mediaFile.cloudState != CloudState.none) { + hasCloudBackup = true; + break; + } + } + + bool? deleteCompletely; + if (hasCloudBackup) { + deleteCompletely = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.lang.deleteImageTitle), + content: Text(context.lang.deleteMemoriesBody(count)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.lang.galleryCancel), + ), + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Local Only'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Completely'), + ), + ], + ), + ); + } else { + final confirmed = await showAlertDialog( + context, + context.lang.deleteImageTitle, + context.lang.deleteMemoriesBody(count), + ); + if (confirmed) deleteCompletely = true; + } + + if (deleteCompletely == null) return; + await _showProgressDialog( 'Deleting memories...', (setProgress) async { @@ -275,9 +331,14 @@ class MemoriesViewState extends State with AutomaticKeepAliveClien .where((e) => e.mediaService.mediaFile.mediaId == mediaId) .firstOrNull; if (item != null) { - item.mediaService.fullMediaRemoval(); + if (deleteCompletely!) { + item.mediaService.fullMediaRemoval(); + await apiService.deleteMemory(mediaId); + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); + } else { + item.mediaService.storedPath.deleteSync(); + } } - await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); setProgress((i + 1) / selectedList.length); } }, @@ -406,10 +467,8 @@ class MemoriesViewState extends State with AutomaticKeepAliveClien Text( context.lang.memoriesEmpty, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.grey.shade600, - fontWeight: FontWeight.w500, + style: const TextStyle( + color: Colors.grey, ), ), ], @@ -446,156 +505,201 @@ class MemoriesViewState extends State with AutomaticKeepAliveClien orderedByMonth = filteredOrdered; } - return LayoutBuilder( - builder: (context, constraints) { - return DraggableScrollbar( - controller: _scrollController, - labelBuilder: (offset) { - final state = _service.currentState; - if (state.isEmpty || state.months.isEmpty) return null; + return Column( + children: [ + if (_isUsageLimitReached) + Container( + color: Colors.redAccent, + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: const Text( + 'Cloud backup limit reached! Please upgrade your plan or free up space.', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return DraggableScrollbar( + controller: _scrollController, + labelBuilder: (offset) { + final state = _service.currentState; + if (state.isEmpty || state.months.isEmpty) { + return null; + } - // Simple heuristic to find month by offset - double currentOffset = 56; - if (state.galleryItemsLastYears.isNotEmpty) { - currentOffset += 220; - } + // Simple heuristic to find month by offset + double currentOffset = 56; + if (state.galleryItemsLastYears.isNotEmpty) { + currentOffset += 220; + } - final screenWidth = MediaQuery.sizeOf(context).width; - final itemWidth = (screenWidth - 8) / 4; - final itemHeight = itemWidth * (16 / 9); - final rowHeight = itemHeight + 2; + final screenWidth = MediaQuery.sizeOf( + context, + ).width; + final itemWidth = (screenWidth - 8) / 4; + final itemHeight = itemWidth * (16 / 9); + final rowHeight = itemHeight + 2; - for (final month in state.months) { - final indices = state.orderedByMonth[month]!; - final totalRows = (indices.length + 3) ~/ 4; - final monthHeight = 44 + (totalRows * rowHeight); + for (final month in state.months) { + final indices = state.orderedByMonth[month]!; + final totalRows = (indices.length + 3) ~/ 4; + final monthHeight = 44 + (totalRows * rowHeight); - if (offset < currentOffset + monthHeight) { - return month; - } - currentOffset += monthHeight; - } - return state.months.last; - }, - child: CustomScrollView( - controller: _scrollController, - physics: const BouncingScrollPhysics(), - slivers: [ - SliverAppBar( - title: const Text( - 'Memories', - style: TextStyle(fontWeight: FontWeight.bold), - ), - floating: true, - snap: true, - elevation: 0, - backgroundColor: context.color.surface, - actions: [ - if (state.isLoading) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, + if (offset < currentOffset + monthHeight) { + return month; + } + currentOffset += monthHeight; + } + return state.months.last; + }, + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + SliverAppBar( + title: const Text( + 'Memories', + style: TextStyle(fontWeight: FontWeight.bold), ), - child: Center( - child: Tooltip( - message: context.lang.migrationOfMemories( - state.filesToMigrate, - ), - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - value: state.migrationProgress, - strokeWidth: 2.5, - valueColor: AlwaysStoppedAnimation( - context.color.primary, + floating: true, + snap: true, + elevation: 0, + backgroundColor: context.color.surface, + actions: [ + if (state.isLoading) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Center( + child: Tooltip( + message: context.lang + .migrationOfMemories( + state.filesToMigrate, + ), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + value: state.migrationProgress, + strokeWidth: 2.5, + valueColor: + AlwaysStoppedAnimation( + context.color.primary, + ), + backgroundColor: context + .color + .primary + .withValues(alpha: 0.2), + ), + ), ), - backgroundColor: context.color.primary - .withValues(alpha: 0.2), + ), + ), + IconButton( + icon: Icon( + _filterFavoritesOnly + ? Icons.favorite + : Icons.favorite_border, + color: _filterFavoritesOnly + ? Colors.redAccent + : null, + ), + onPressed: () { + setState(() { + _filterFavoritesOnly = + !_filterFavoritesOnly; + }); + }, + tooltip: _filterFavoritesOnly + ? 'Show all' + : 'Show favorites only', + ), + ], + ), + MemoriesFlashbackBannerComp( + lastYears: lastYears, + onOpenFlashback: (items, idx) => + _openViewer(items, idx, isFlashback: true), + ), + for (final month in months) ...[ + SliverPadding( + padding: const EdgeInsets.fromLTRB( + 8, + 12, + 8, + 6, + ), + sliver: SliverToBoxAdapter( + child: Text( + month, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), ), ), ), - ), - IconButton( - icon: Icon( - _filterFavoritesOnly - ? Icons.favorite - : Icons.favorite_border, - color: _filterFavoritesOnly - ? Colors.redAccent - : null, - ), - onPressed: () { - setState(() { - _filterFavoritesOnly = !_filterFavoritesOnly; - }); - }, - tooltip: _filterFavoritesOnly - ? 'Show all' - : 'Show favorites only', - ), - ], - ), - MemoriesFlashbackBannerComp( - lastYears: lastYears, - onOpenFlashback: (items, idx) => - _openViewer(items, idx, isFlashback: true), - ), - for (final month in months) ...[ - SliverPadding( - padding: const EdgeInsets.fromLTRB(8, 12, 8, 6), - sliver: SliverToBoxAdapter( - child: Text( - month, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - SliverGrid( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - mainAxisSpacing: 2, - crossAxisSpacing: 2, - childAspectRatio: 9 / 16, - ), - delegate: SliverChildBuilderDelegate( - (context, idx) { - final globalIndex = orderedByMonth[month]![idx]; - final item = state.galleryItems[globalIndex]; - final mediaId = - item.mediaService.mediaFile.mediaId; - final isSelected = _selectedMediaIds.contains( - mediaId, - ); + SliverGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 2, + crossAxisSpacing: 2, + childAspectRatio: 9 / 16, + ), + delegate: SliverChildBuilderDelegate( + (context, idx) { + final globalIndex = + orderedByMonth[month]![idx]; + final item = + state.galleryItems[globalIndex]; + final mediaId = + item.mediaService.mediaFile.mediaId; + final isSelected = _selectedMediaIds + .contains( + mediaId, + ); - return MemoriesThumbnailComp( - galleryItem: item, - index: globalIndex, - selectionMode: _selectionMode, - isSelected: isSelected, - activeMediaIdNotifier: _activeMediaIdNotifier, - onLongPress: () => _onLongPressItem(mediaId), - onTap: () => _onTapItem(mediaId, globalIndex), - ); - }, - childCount: orderedByMonth[month]!.length, - ), + return MemoriesThumbnailComp( + galleryItem: item, + index: globalIndex, + selectionMode: _selectionMode, + isSelected: isSelected, + activeMediaIdNotifier: + _activeMediaIdNotifier, + onLongPress: () => + _onLongPressItem(mediaId), + onTap: () => + _onTapItem(mediaId, globalIndex), + ); + }, + childCount: orderedByMonth[month]!.length, + ), + ), + ], + SliverPadding( + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom + + 150, + ), + ), + ], ), - ], - SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 150, - ), - ), - ], + ); + }, ), - ); - }, + ), + ], ); }, ), diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index 52e108e1..97eeca52 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:photo_view/photo_view.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -313,6 +314,9 @@ class _SynchronizedImageViewerScreenState var filePath = item.mediaService.storedPath; if (!filePath.existsSync()) { filePath = item.mediaService.tempPath; + if (!filePath.existsSync()) { + filePath = item.mediaService.thumbnailPath; + } } final isVideo = @@ -335,13 +339,23 @@ class _SynchronizedImageViewerScreenState return childWidget!; }, child: !filePath.existsSync() - ? const Center( - child: Icon( - Icons.broken_image_outlined, - color: Colors.white38, - size: 64, - ), - ) + ? item.mediaService.mediaFile.blurhash != null + ? BlurHash( + hash: item + .mediaService + .mediaFile + .blurhash!, + optimizationMode: + BlurHashOptimizationMode + .approximation, + ) + : const Center( + child: Icon( + Icons.broken_image_outlined, + color: Colors.white38, + size: 64, + ), + ) : isVideo ? VideoPlayerFileHelper(videoPath: filePath) : PhotoView( diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index 2f381f37..a19c0652 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -5,7 +5,10 @@ import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/model/json/backup.model.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' + as server; import 'package:twonly/src/services/backup.service.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart'; @@ -21,6 +24,7 @@ class BackupView extends StatefulWidget { class _BackupViewState extends State { bool _isLoading = false; CurrentBackupStatus? _backupStatus; + server.Response_MemoriesUsage? _memoriesUsage; StreamSubscription? _backupUpdateSub; @override @@ -41,9 +45,11 @@ class _BackupViewState extends State { Future _loadBackupStatus() async { setState(() => _isLoading = true); final status = await BackupService.getData(); + final memoriesUsage = await apiService.getMemoriesUsage(); if (!mounted) return; setState(() { _backupStatus = status; + _memoriesUsage = memoriesUsage; _isLoading = false; }); } @@ -182,6 +188,58 @@ class _BackupViewState extends State { ), ]), ), + const SizedBox(height: 24), + const Center( + child: Text( + 'Memories Backup', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 8), + Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: _buildTableRows([ + ( + 'Usage', + _memoriesUsage != null + ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' + : '-', + ), + ( + 'Files Backed Up', + _memoriesUsage != null + ? '${_memoriesUsage!.count}' + : '-', + ), + ]), + ), + StreamBuilder( + initialData: memoriesCloudService.currentProgress, + stream: memoriesCloudService.progressStream, + builder: (context, snapshot) { + final progress = snapshot.data; + if (progress == null || progress.totalPending == 0) { + return const SizedBox.shrink(); + } + final percent = (progress.currentUploaded / progress.totalPending) + + (progress.currentUploadProgress / progress.totalPending); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text( + 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: percent.clamp(0.0, 1.0), + ), + ], + ); + }, + ), ], ), diff --git a/lib/src/visual/views/settings/developer/developer.view.dart b/lib/src/visual/views/settings/developer/developer.view.dart index bc6d203b..c8be5d6f 100644 --- a/lib/src/visual/views/settings/developer/developer.view.dart +++ b/lib/src/visual/views/settings/developer/developer.view.dart @@ -221,6 +221,7 @@ class _DeveloperSettingsViewState extends State { isFavorite: false, hasCropAnalyzed: false, hasThumbnail: false, + cloudState: CloudState.none, createdAt: now, ); final mediaService = MediaFileService(mediaFile); diff --git a/memories.md b/memories.md new file mode 100644 index 00000000..49400289 --- /dev/null +++ b/memories.md @@ -0,0 +1,112 @@ +# Memories API Documentation + +The **Memories** feature allows users to back up their media to the server in an end-to-end encrypted format. The client-server communication happens primarily via the existing WebSocket connection using Protocol Buffers (`client_to_server.proto` and `server_to_client.proto`), while the actual binary file uploads/downloads use Amazon S3 presigned URLs via HTTP PUT/GET. + +## 1. Checking Storage Quota +Before allowing a user to upload a memory, the client should query the user's current storage usage to ensure they haven't exceeded their limit. + +**Request:** +```proto +// client_to_server.proto +message GetMemoriesUsage {} +``` +**Response:** +```proto +// server_to_client.proto +message MemoriesUsage { + int64 max_bytes = 1; + int64 current_bytes = 2; + int64 count = 3; +} +``` +If `current_bytes` + the new file size > `max_bytes`, the client should block the upload and optionally prompt the user to upgrade their plan or free up space. + +## 2. Uploading a Memory +Uploading a memory is a three-step process: requesting presigned URLs, performing the actual HTTP uploads, and finally confirming the upload. + +### Step 2.1: Request Upload URLs +The client requests permission to upload a new memory item, declaring its size and original capture date. + +**Request:** +```proto +// client_to_server.proto +message RequestMemoriesUpload { + int64 size = 1; // Size of the FULL media file in bytes + int64 original_date = 2; // Unix timestamp (in milliseconds/seconds depending on your app's standard) of when the media was originally created +} +``` + +**Response:** +```proto +// server_to_client.proto +message MemoriesUploadUrls { + string media_id = 1; // Unique identifier generated by the server for this memory + string thumbnail_upload_url = 2; // S3 presigned URL for uploading the encrypted thumbnail + string full_upload_url = 3; // S3 presigned URL for uploading the encrypted full media +} +``` + +### Step 2.2: Perform HTTP Uploads +Using the URLs returned in Step 2.1, the client encrypts the thumbnail and the full media locally, and performs an HTTP `PUT` request to the respective URLs. + +*Note: The upload URLs are usually valid only for a short time (e.g. 15-60 minutes). Ensure you upload the data promptly.* + +### Step 2.3: Confirm the Upload +After both the thumbnail and the full media are successfully uploaded via HTTP, the client **must** notify the server so it can mark the memory as fully uploaded and available. + +**Request:** +```proto +// client_to_server.proto +message ConfirmMemoriesUpload { + string media_id = 1; // The media_id obtained in Step 2.1 +} +``` +*(The server will respond with an empty `Ok::None(true)` acknowledgment upon success).* + + +## 3. Retrieving Memories (Pagination) +To display a gallery or timeline of backed-up memories, the client requests a paginated list of media items. The endpoint returns a descending list (newest first) based on the original date. + +**Request:** +```proto +// client_to_server.proto +message GetMemoriesList { + int64 offset_date = 1; // Unix timestamp to fetch memories older than this date (for pagination). Use MAX_INT64 for the first page. + int64 limit = 2; // Maximum number of items to return in this batch (e.g. 50) +} +``` + +**Response:** +```proto +// server_to_client.proto +message MemoriesList { + repeated MediaItem items = 1; +} + +message MediaItem { + string media_id = 1; + int64 original_date = 2; + string thumbnail_download_url = 3; // S3 presigned URL to download the encrypted thumbnail +} +``` +The client can immediately use the `thumbnail_download_url` to fetch and decrypt the thumbnail for the gallery view. + +## 4. Downloading Full Media +When a user taps on a memory to view it in full screen, the client requests a temporary download URL for the full-resolution file. + +**Request:** +```proto +// client_to_server.proto +message GetMemoriesUrl { + string media_id = 1; +} +``` + +**Response:** +```proto +// server_to_client.proto +message MemoriesUrl { + string full_download_url = 1; // S3 presigned URL to download the encrypted full media +} +``` +The client can then perform an HTTP `GET` request to `full_download_url`, decrypt the payload locally, and display the full-resolution image or video. diff --git a/pubspec.lock b/pubspec.lock index adc20a54..7d2ea56e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -112,6 +112,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.5.4" + blurhash_dart: + dependency: "direct main" + description: + name: blurhash_dart + sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f" + url: "https://pub.dev" + source: hosted + version: "1.2.1" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 21d16322..1e6a3b7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -113,6 +113,7 @@ dependencies: rust_lib_twonly: path: rust_builder flutter_rust_bridge: 2.12.0 + blurhash_dart: ^1.2.1 dependency_overrides: diff --git a/rust/src/bridge/wrapper/key_manager.rs b/rust/src/bridge/wrapper/key_manager.rs index 06ef4490..9d1b7354 100644 --- a/rust/src/bridge/wrapper/key_manager.rs +++ b/rust/src/bridge/wrapper/key_manager.rs @@ -124,5 +124,21 @@ impl RustKeyManager { *ctx.key_manager.lock().await = key_manager; Ok(()) } + + pub async fn encrypt_cloud_media_key(media_key: Vec, addition: String) -> Result> { + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + if media_key.len() != 32 { + return Err(TwonlyError::WronKeySize(32, media_key.len())); + } + let mut key_array = [0u8; 32]; + key_array.copy_from_slice(&media_key); + Ok(key_manager.main_key.encrypt_cloud_media_key(&key_array, &addition)) + } + + pub async fn decrypt_cloud_media_key(encrypted_media_key: Vec, addition: String) -> Result> { + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + let decrypted = key_manager.main_key.decrypt_cloud_media_key(&encrypted_media_key, &addition)?; + Ok(decrypted.to_vec()) + } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index c95d58d1..884c55e8 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 340781866; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1788847092; // Section: executor @@ -380,6 +380,38 @@ let api_password = ::sse_decode(&mut deserializer);deserializer.end(); m })().await) } }) } +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_decrypt_cloud_media_key", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_encrypted_media_key = >::sse_decode(&mut deserializer); +let api_addition = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::decrypt_cloud_media_key(api_encrypted_media_key, api_addition).await?; Ok(output_ok) + })().await) + } }) +} +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_encrypt_cloud_media_key", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_media_key = >::sse_decode(&mut deserializer); +let api_addition = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::encrypt_cloud_media_key(api_media_key, api_addition).await?; Ok(output_ok) + })().await) + } }) +} fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1866,36 +1898,38 @@ fn pde_ffi_dispatcher_primary_impl( 15 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 16 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), 17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), -19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), -25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), -26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -28 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -29 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -30 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -31 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -32 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len), -33 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len), -34 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len), -35 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len), -36 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len), -37 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len), -38 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len), -39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len), -40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len), -41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len), -42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len), -43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len), -44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len), -45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len), -46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len), -47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len), +18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), +28 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +29 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +30 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +31 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +32 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +33 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +34 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len), +35 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len), +36 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len), +37 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len), +38 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len), +39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len), +40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len), +41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len), +42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len), +43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len), +44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len), +45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len), +46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len), +47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len), +48 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len), +49 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } diff --git a/rust/src/keys/main_key.rs b/rust/src/keys/main_key.rs index 0a3960ce..d1b10160 100644 --- a/rust/src/keys/main_key.rs +++ b/rust/src/keys/main_key.rs @@ -61,23 +61,27 @@ impl MainKey { self.decrypt_with_info(b"backup_key", encrypted_backup) } - // Encrypts a newly generated media key using the derived Media Main Key. - // pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec { - // self.encrypt_with_info(b"media_main_key", media_key) - // } + pub fn encrypt_cloud_media_key(&self, media_key: &[u8; 32], addition: &str) -> Vec { + let info = format!("cloud_media_key_{}", addition); + self.encrypt_with_info(info.as_bytes(), media_key) + } - // Decrypts a wrapped media key using the derived Media Main Key. - // pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> { - // let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?; + pub fn decrypt_cloud_media_key( + &self, + encrypted_media_key: &[u8], + addition: &str, + ) -> Result<[u8; 32]> { + let info = format!("cloud_media_key_{}", addition); + let decrypted = self.decrypt_with_info(info.as_bytes(), encrypted_media_key)?; - // if decrypted.len() != 32 { - // return Err("Invalid decrypted key length".to_string())?; - // } + if decrypted.len() != 32 { + return Err("Invalid decrypted key length".to_string())?; + } - // let mut result = [0u8; 32]; - // result.copy_from_slice(&decrypted); - // Ok(result) - // } + let mut result = [0u8; 32]; + result.copy_from_slice(&decrypted); + Ok(result) + } fn derive_key(&self, info: &[u8]) -> [u8; 32] { let hk = Hkdf::::new(None, &self.main_key); diff --git a/test/drift/twonly_db/generated/schema.dart b/test/drift/twonly_db/generated/schema.dart index 51c0ac10..0b68a8fe 100644 --- a/test/drift/twonly_db/generated/schema.dart +++ b/test/drift/twonly_db/generated/schema.dart @@ -26,6 +26,7 @@ import 'schema_v19.dart' as v19; import 'schema_v20.dart' as v20; import 'schema_v21.dart' as v21; import 'schema_v22.dart' as v22; +import 'schema_v23.dart' as v23; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -75,6 +76,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v21.DatabaseAtV21(db); case 22: return v22.DatabaseAtV22(db); + case 23: + return v23.DatabaseAtV23(db); default: throw MissingSchemaException(version, versions); } @@ -103,5 +106,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 20, 21, 22, + 23, ]; } diff --git a/test/drift/twonly_db/generated/schema_v23.dart b/test/drift/twonly_db/generated/schema_v23.dart new file mode 100644 index 00000000..f339c8e5 --- /dev/null +++ b/test/drift/twonly_db/generated/schema_v23.dart @@ -0,0 +1,11135 @@ +// dart format width=80 +import 'dart:typed_data' as i2; +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Contacts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Contacts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn username = GeneratedColumn( + 'username', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn nickName = GeneratedColumn( + 'nick_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn avatarSvgCompressed = + GeneratedColumn( + 'avatar_svg_compressed', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn senderProfileCounter = GeneratedColumn( + 'sender_profile_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn accepted = GeneratedColumn( + 'accepted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (accepted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn deletedByUser = GeneratedColumn( + 'deleted_by_user', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (deleted_by_user IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn requested = GeneratedColumn( + 'requested', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (requested IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (blocked IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn verified = GeneratedColumn( + 'verified', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (verified IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn accountDeleted = GeneratedColumn( + 'account_deleted', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (account_deleted IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn userDiscoveryVersion = + GeneratedColumn( + 'user_discovery_version', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn userDiscoveryExcluded = GeneratedColumn( + 'user_discovery_excluded', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn userDiscoveryManualApproved = + GeneratedColumn( + 'user_discovery_manual_approved', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NULL DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn recoveryIsTrustedFriend = + GeneratedColumn( + 'recovery_is_trusted_friend', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (recovery_is_trusted_friend IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn recoveryLastHeartbeat = GeneratedColumn( + 'recovery_last_heartbeat', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn recoverySecretShare = + GeneratedColumn( + 'recovery_secret_share', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn recoveryContactsSecretShare = + GeneratedColumn( + 'recovery_contacts_secret_share', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn recoveryContactsLastHeartbeat = + GeneratedColumn( + 'recovery_contacts_last_heartbeat', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn recoveryContactsThreshold = + GeneratedColumn( + 'recovery_contacts_threshold', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn askForFriendPromotions = GeneratedColumn( + 'ask_for_friend_promotions', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (ask_for_friend_promotions IN (0, 1))', + ); + late final GeneratedColumn mediaSendCounter = GeneratedColumn( + 'media_send_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn mediaReceivedCounter = GeneratedColumn( + 'media_received_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + userId, + username, + displayName, + nickName, + avatarSvgCompressed, + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt, + userDiscoveryVersion, + userDiscoveryExcluded, + userDiscoveryManualApproved, + recoveryIsTrustedFriend, + recoveryLastHeartbeat, + recoverySecretShare, + recoveryContactsSecretShare, + recoveryContactsLastHeartbeat, + recoveryContactsThreshold, + askForFriendPromotions, + mediaSendCounter, + mediaReceivedCounter, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'contacts'; + @override + Set get $primaryKey => {userId}; + @override + ContactsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContactsData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_id'], + )!, + username: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}username'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + ), + nickName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}nick_name'], + ), + avatarSvgCompressed: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}avatar_svg_compressed'], + ), + senderProfileCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sender_profile_counter'], + )!, + accepted: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}accepted'], + )!, + deletedByUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_by_user'], + )!, + requested: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}requested'], + )!, + blocked: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}blocked'], + )!, + verified: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verified'], + )!, + accountDeleted: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}account_deleted'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + userDiscoveryVersion: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}user_discovery_version'], + ), + userDiscoveryExcluded: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_excluded'], + )!, + userDiscoveryManualApproved: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}user_discovery_manual_approved'], + ), + recoveryIsTrustedFriend: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}recovery_is_trusted_friend'], + )!, + recoveryLastHeartbeat: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}recovery_last_heartbeat'], + ), + recoverySecretShare: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}recovery_secret_share'], + ), + recoveryContactsSecretShare: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}recovery_contacts_secret_share'], + ), + recoveryContactsLastHeartbeat: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}recovery_contacts_last_heartbeat'], + ), + recoveryContactsThreshold: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}recovery_contacts_threshold'], + ), + askForFriendPromotions: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ask_for_friend_promotions'], + ), + mediaSendCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_send_counter'], + )!, + mediaReceivedCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_received_counter'], + )!, + ); + } + + @override + Contacts createAlias(String alias) { + return Contacts(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(user_id)']; + @override + bool get dontWriteConstraints => true; +} + +class ContactsData extends DataClass implements Insertable { + final int userId; + final String username; + final String? displayName; + final String? nickName; + final i2.Uint8List? avatarSvgCompressed; + final int senderProfileCounter; + final int accepted; + final int deletedByUser; + final int requested; + final int blocked; + final int verified; + final int accountDeleted; + final int createdAt; + final i2.Uint8List? userDiscoveryVersion; + final int userDiscoveryExcluded; + final int? userDiscoveryManualApproved; + final int recoveryIsTrustedFriend; + final int? recoveryLastHeartbeat; + final i2.Uint8List? recoverySecretShare; + final i2.Uint8List? recoveryContactsSecretShare; + final int? recoveryContactsLastHeartbeat; + final int? recoveryContactsThreshold; + final int? askForFriendPromotions; + final int mediaSendCounter; + final int mediaReceivedCounter; + const ContactsData({ + required this.userId, + required this.username, + this.displayName, + this.nickName, + this.avatarSvgCompressed, + required this.senderProfileCounter, + required this.accepted, + required this.deletedByUser, + required this.requested, + required this.blocked, + required this.verified, + required this.accountDeleted, + required this.createdAt, + this.userDiscoveryVersion, + required this.userDiscoveryExcluded, + this.userDiscoveryManualApproved, + required this.recoveryIsTrustedFriend, + this.recoveryLastHeartbeat, + this.recoverySecretShare, + this.recoveryContactsSecretShare, + this.recoveryContactsLastHeartbeat, + this.recoveryContactsThreshold, + this.askForFriendPromotions, + required this.mediaSendCounter, + required this.mediaReceivedCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['username'] = Variable(username); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + if (!nullToAbsent || nickName != null) { + map['nick_name'] = Variable(nickName); + } + if (!nullToAbsent || avatarSvgCompressed != null) { + map['avatar_svg_compressed'] = Variable( + avatarSvgCompressed, + ); + } + map['sender_profile_counter'] = Variable(senderProfileCounter); + map['accepted'] = Variable(accepted); + map['deleted_by_user'] = Variable(deletedByUser); + map['requested'] = Variable(requested); + map['blocked'] = Variable(blocked); + map['verified'] = Variable(verified); + map['account_deleted'] = Variable(accountDeleted); + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || userDiscoveryVersion != null) { + map['user_discovery_version'] = Variable( + userDiscoveryVersion, + ); + } + map['user_discovery_excluded'] = Variable(userDiscoveryExcluded); + if (!nullToAbsent || userDiscoveryManualApproved != null) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved, + ); + } + map['recovery_is_trusted_friend'] = Variable(recoveryIsTrustedFriend); + if (!nullToAbsent || recoveryLastHeartbeat != null) { + map['recovery_last_heartbeat'] = Variable(recoveryLastHeartbeat); + } + if (!nullToAbsent || recoverySecretShare != null) { + map['recovery_secret_share'] = Variable( + recoverySecretShare, + ); + } + if (!nullToAbsent || recoveryContactsSecretShare != null) { + map['recovery_contacts_secret_share'] = Variable( + recoveryContactsSecretShare, + ); + } + if (!nullToAbsent || recoveryContactsLastHeartbeat != null) { + map['recovery_contacts_last_heartbeat'] = Variable( + recoveryContactsLastHeartbeat, + ); + } + if (!nullToAbsent || recoveryContactsThreshold != null) { + map['recovery_contacts_threshold'] = Variable( + recoveryContactsThreshold, + ); + } + if (!nullToAbsent || askForFriendPromotions != null) { + map['ask_for_friend_promotions'] = Variable(askForFriendPromotions); + } + map['media_send_counter'] = Variable(mediaSendCounter); + map['media_received_counter'] = Variable(mediaReceivedCounter); + return map; + } + + ContactsCompanion toCompanion(bool nullToAbsent) { + return ContactsCompanion( + userId: Value(userId), + username: Value(username), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + nickName: nickName == null && nullToAbsent + ? const Value.absent() + : Value(nickName), + avatarSvgCompressed: avatarSvgCompressed == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvgCompressed), + senderProfileCounter: Value(senderProfileCounter), + accepted: Value(accepted), + deletedByUser: Value(deletedByUser), + requested: Value(requested), + blocked: Value(blocked), + verified: Value(verified), + accountDeleted: Value(accountDeleted), + createdAt: Value(createdAt), + userDiscoveryVersion: userDiscoveryVersion == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryVersion), + userDiscoveryExcluded: Value(userDiscoveryExcluded), + userDiscoveryManualApproved: + userDiscoveryManualApproved == null && nullToAbsent + ? const Value.absent() + : Value(userDiscoveryManualApproved), + recoveryIsTrustedFriend: Value(recoveryIsTrustedFriend), + recoveryLastHeartbeat: recoveryLastHeartbeat == null && nullToAbsent + ? const Value.absent() + : Value(recoveryLastHeartbeat), + recoverySecretShare: recoverySecretShare == null && nullToAbsent + ? const Value.absent() + : Value(recoverySecretShare), + recoveryContactsSecretShare: + recoveryContactsSecretShare == null && nullToAbsent + ? const Value.absent() + : Value(recoveryContactsSecretShare), + recoveryContactsLastHeartbeat: + recoveryContactsLastHeartbeat == null && nullToAbsent + ? const Value.absent() + : Value(recoveryContactsLastHeartbeat), + recoveryContactsThreshold: + recoveryContactsThreshold == null && nullToAbsent + ? const Value.absent() + : Value(recoveryContactsThreshold), + askForFriendPromotions: askForFriendPromotions == null && nullToAbsent + ? const Value.absent() + : Value(askForFriendPromotions), + mediaSendCounter: Value(mediaSendCounter), + mediaReceivedCounter: Value(mediaReceivedCounter), + ); + } + + factory ContactsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ContactsData( + userId: serializer.fromJson(json['userId']), + username: serializer.fromJson(json['username']), + displayName: serializer.fromJson(json['displayName']), + nickName: serializer.fromJson(json['nickName']), + avatarSvgCompressed: serializer.fromJson( + json['avatarSvgCompressed'], + ), + senderProfileCounter: serializer.fromJson( + json['senderProfileCounter'], + ), + accepted: serializer.fromJson(json['accepted']), + deletedByUser: serializer.fromJson(json['deletedByUser']), + requested: serializer.fromJson(json['requested']), + blocked: serializer.fromJson(json['blocked']), + verified: serializer.fromJson(json['verified']), + accountDeleted: serializer.fromJson(json['accountDeleted']), + createdAt: serializer.fromJson(json['createdAt']), + userDiscoveryVersion: serializer.fromJson( + json['userDiscoveryVersion'], + ), + userDiscoveryExcluded: serializer.fromJson( + json['userDiscoveryExcluded'], + ), + userDiscoveryManualApproved: serializer.fromJson( + json['userDiscoveryManualApproved'], + ), + recoveryIsTrustedFriend: serializer.fromJson( + json['recoveryIsTrustedFriend'], + ), + recoveryLastHeartbeat: serializer.fromJson( + json['recoveryLastHeartbeat'], + ), + recoverySecretShare: serializer.fromJson( + json['recoverySecretShare'], + ), + recoveryContactsSecretShare: serializer.fromJson( + json['recoveryContactsSecretShare'], + ), + recoveryContactsLastHeartbeat: serializer.fromJson( + json['recoveryContactsLastHeartbeat'], + ), + recoveryContactsThreshold: serializer.fromJson( + json['recoveryContactsThreshold'], + ), + askForFriendPromotions: serializer.fromJson( + json['askForFriendPromotions'], + ), + mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), + mediaReceivedCounter: serializer.fromJson( + json['mediaReceivedCounter'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'username': serializer.toJson(username), + 'displayName': serializer.toJson(displayName), + 'nickName': serializer.toJson(nickName), + 'avatarSvgCompressed': serializer.toJson( + avatarSvgCompressed, + ), + 'senderProfileCounter': serializer.toJson(senderProfileCounter), + 'accepted': serializer.toJson(accepted), + 'deletedByUser': serializer.toJson(deletedByUser), + 'requested': serializer.toJson(requested), + 'blocked': serializer.toJson(blocked), + 'verified': serializer.toJson(verified), + 'accountDeleted': serializer.toJson(accountDeleted), + 'createdAt': serializer.toJson(createdAt), + 'userDiscoveryVersion': serializer.toJson( + userDiscoveryVersion, + ), + 'userDiscoveryExcluded': serializer.toJson(userDiscoveryExcluded), + 'userDiscoveryManualApproved': serializer.toJson( + userDiscoveryManualApproved, + ), + 'recoveryIsTrustedFriend': serializer.toJson( + recoveryIsTrustedFriend, + ), + 'recoveryLastHeartbeat': serializer.toJson(recoveryLastHeartbeat), + 'recoverySecretShare': serializer.toJson( + recoverySecretShare, + ), + 'recoveryContactsSecretShare': serializer.toJson( + recoveryContactsSecretShare, + ), + 'recoveryContactsLastHeartbeat': serializer.toJson( + recoveryContactsLastHeartbeat, + ), + 'recoveryContactsThreshold': serializer.toJson( + recoveryContactsThreshold, + ), + 'askForFriendPromotions': serializer.toJson(askForFriendPromotions), + 'mediaSendCounter': serializer.toJson(mediaSendCounter), + 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), + }; + } + + ContactsData copyWith({ + int? userId, + String? username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), + int? senderProfileCounter, + int? accepted, + int? deletedByUser, + int? requested, + int? blocked, + int? verified, + int? accountDeleted, + int? createdAt, + Value userDiscoveryVersion = const Value.absent(), + int? userDiscoveryExcluded, + Value userDiscoveryManualApproved = const Value.absent(), + int? recoveryIsTrustedFriend, + Value recoveryLastHeartbeat = const Value.absent(), + Value recoverySecretShare = const Value.absent(), + Value recoveryContactsSecretShare = const Value.absent(), + Value recoveryContactsLastHeartbeat = const Value.absent(), + Value recoveryContactsThreshold = const Value.absent(), + Value askForFriendPromotions = const Value.absent(), + int? mediaSendCounter, + int? mediaReceivedCounter, + }) => ContactsData( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName.present ? displayName.value : this.displayName, + nickName: nickName.present ? nickName.value : this.nickName, + avatarSvgCompressed: avatarSvgCompressed.present + ? avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + userDiscoveryVersion: userDiscoveryVersion.present + ? userDiscoveryVersion.value + : this.userDiscoveryVersion, + userDiscoveryExcluded: userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: userDiscoveryManualApproved.present + ? userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, + recoveryIsTrustedFriend: + recoveryIsTrustedFriend ?? this.recoveryIsTrustedFriend, + recoveryLastHeartbeat: recoveryLastHeartbeat.present + ? recoveryLastHeartbeat.value + : this.recoveryLastHeartbeat, + recoverySecretShare: recoverySecretShare.present + ? recoverySecretShare.value + : this.recoverySecretShare, + recoveryContactsSecretShare: recoveryContactsSecretShare.present + ? recoveryContactsSecretShare.value + : this.recoveryContactsSecretShare, + recoveryContactsLastHeartbeat: recoveryContactsLastHeartbeat.present + ? recoveryContactsLastHeartbeat.value + : this.recoveryContactsLastHeartbeat, + recoveryContactsThreshold: recoveryContactsThreshold.present + ? recoveryContactsThreshold.value + : this.recoveryContactsThreshold, + askForFriendPromotions: askForFriendPromotions.present + ? askForFriendPromotions.value + : this.askForFriendPromotions, + mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, + mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, + ); + ContactsData copyWithCompanion(ContactsCompanion data) { + return ContactsData( + userId: data.userId.present ? data.userId.value : this.userId, + username: data.username.present ? data.username.value : this.username, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + nickName: data.nickName.present ? data.nickName.value : this.nickName, + avatarSvgCompressed: data.avatarSvgCompressed.present + ? data.avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: data.senderProfileCounter.present + ? data.senderProfileCounter.value + : this.senderProfileCounter, + accepted: data.accepted.present ? data.accepted.value : this.accepted, + deletedByUser: data.deletedByUser.present + ? data.deletedByUser.value + : this.deletedByUser, + requested: data.requested.present ? data.requested.value : this.requested, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + verified: data.verified.present ? data.verified.value : this.verified, + accountDeleted: data.accountDeleted.present + ? data.accountDeleted.value + : this.accountDeleted, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + userDiscoveryVersion: data.userDiscoveryVersion.present + ? data.userDiscoveryVersion.value + : this.userDiscoveryVersion, + userDiscoveryExcluded: data.userDiscoveryExcluded.present + ? data.userDiscoveryExcluded.value + : this.userDiscoveryExcluded, + userDiscoveryManualApproved: data.userDiscoveryManualApproved.present + ? data.userDiscoveryManualApproved.value + : this.userDiscoveryManualApproved, + recoveryIsTrustedFriend: data.recoveryIsTrustedFriend.present + ? data.recoveryIsTrustedFriend.value + : this.recoveryIsTrustedFriend, + recoveryLastHeartbeat: data.recoveryLastHeartbeat.present + ? data.recoveryLastHeartbeat.value + : this.recoveryLastHeartbeat, + recoverySecretShare: data.recoverySecretShare.present + ? data.recoverySecretShare.value + : this.recoverySecretShare, + recoveryContactsSecretShare: data.recoveryContactsSecretShare.present + ? data.recoveryContactsSecretShare.value + : this.recoveryContactsSecretShare, + recoveryContactsLastHeartbeat: data.recoveryContactsLastHeartbeat.present + ? data.recoveryContactsLastHeartbeat.value + : this.recoveryContactsLastHeartbeat, + recoveryContactsThreshold: data.recoveryContactsThreshold.present + ? data.recoveryContactsThreshold.value + : this.recoveryContactsThreshold, + askForFriendPromotions: data.askForFriendPromotions.present + ? data.askForFriendPromotions.value + : this.askForFriendPromotions, + mediaSendCounter: data.mediaSendCounter.present + ? data.mediaSendCounter.value + : this.mediaSendCounter, + mediaReceivedCounter: data.mediaReceivedCounter.present + ? data.mediaReceivedCounter.value + : this.mediaReceivedCounter, + ); + } + + @override + String toString() { + return (StringBuffer('ContactsData(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt, ') + ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') + ..write('recoveryIsTrustedFriend: $recoveryIsTrustedFriend, ') + ..write('recoveryLastHeartbeat: $recoveryLastHeartbeat, ') + ..write('recoverySecretShare: $recoverySecretShare, ') + ..write('recoveryContactsSecretShare: $recoveryContactsSecretShare, ') + ..write( + 'recoveryContactsLastHeartbeat: $recoveryContactsLastHeartbeat, ', + ) + ..write('recoveryContactsThreshold: $recoveryContactsThreshold, ') + ..write('askForFriendPromotions: $askForFriendPromotions, ') + ..write('mediaSendCounter: $mediaSendCounter, ') + ..write('mediaReceivedCounter: $mediaReceivedCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + userId, + username, + displayName, + nickName, + $driftBlobEquality.hash(avatarSvgCompressed), + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt, + $driftBlobEquality.hash(userDiscoveryVersion), + userDiscoveryExcluded, + userDiscoveryManualApproved, + recoveryIsTrustedFriend, + recoveryLastHeartbeat, + $driftBlobEquality.hash(recoverySecretShare), + $driftBlobEquality.hash(recoveryContactsSecretShare), + recoveryContactsLastHeartbeat, + recoveryContactsThreshold, + askForFriendPromotions, + mediaSendCounter, + mediaReceivedCounter, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContactsData && + other.userId == this.userId && + other.username == this.username && + other.displayName == this.displayName && + other.nickName == this.nickName && + $driftBlobEquality.equals( + other.avatarSvgCompressed, + this.avatarSvgCompressed, + ) && + other.senderProfileCounter == this.senderProfileCounter && + other.accepted == this.accepted && + other.deletedByUser == this.deletedByUser && + other.requested == this.requested && + other.blocked == this.blocked && + other.verified == this.verified && + other.accountDeleted == this.accountDeleted && + other.createdAt == this.createdAt && + $driftBlobEquality.equals( + other.userDiscoveryVersion, + this.userDiscoveryVersion, + ) && + other.userDiscoveryExcluded == this.userDiscoveryExcluded && + other.userDiscoveryManualApproved == + this.userDiscoveryManualApproved && + other.recoveryIsTrustedFriend == this.recoveryIsTrustedFriend && + other.recoveryLastHeartbeat == this.recoveryLastHeartbeat && + $driftBlobEquality.equals( + other.recoverySecretShare, + this.recoverySecretShare, + ) && + $driftBlobEquality.equals( + other.recoveryContactsSecretShare, + this.recoveryContactsSecretShare, + ) && + other.recoveryContactsLastHeartbeat == + this.recoveryContactsLastHeartbeat && + other.recoveryContactsThreshold == this.recoveryContactsThreshold && + other.askForFriendPromotions == this.askForFriendPromotions && + other.mediaSendCounter == this.mediaSendCounter && + other.mediaReceivedCounter == this.mediaReceivedCounter); +} + +class ContactsCompanion extends UpdateCompanion { + final Value userId; + final Value username; + final Value displayName; + final Value nickName; + final Value avatarSvgCompressed; + final Value senderProfileCounter; + final Value accepted; + final Value deletedByUser; + final Value requested; + final Value blocked; + final Value verified; + final Value accountDeleted; + final Value createdAt; + final Value userDiscoveryVersion; + final Value userDiscoveryExcluded; + final Value userDiscoveryManualApproved; + final Value recoveryIsTrustedFriend; + final Value recoveryLastHeartbeat; + final Value recoverySecretShare; + final Value recoveryContactsSecretShare; + final Value recoveryContactsLastHeartbeat; + final Value recoveryContactsThreshold; + final Value askForFriendPromotions; + final Value mediaSendCounter; + final Value mediaReceivedCounter; + const ContactsCompanion({ + this.userId = const Value.absent(), + this.username = const Value.absent(), + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), + this.recoveryIsTrustedFriend = const Value.absent(), + this.recoveryLastHeartbeat = const Value.absent(), + this.recoverySecretShare = const Value.absent(), + this.recoveryContactsSecretShare = const Value.absent(), + this.recoveryContactsLastHeartbeat = const Value.absent(), + this.recoveryContactsThreshold = const Value.absent(), + this.askForFriendPromotions = const Value.absent(), + this.mediaSendCounter = const Value.absent(), + this.mediaReceivedCounter = const Value.absent(), + }); + ContactsCompanion.insert({ + this.userId = const Value.absent(), + required String username, + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + this.userDiscoveryVersion = const Value.absent(), + this.userDiscoveryExcluded = const Value.absent(), + this.userDiscoveryManualApproved = const Value.absent(), + this.recoveryIsTrustedFriend = const Value.absent(), + this.recoveryLastHeartbeat = const Value.absent(), + this.recoverySecretShare = const Value.absent(), + this.recoveryContactsSecretShare = const Value.absent(), + this.recoveryContactsLastHeartbeat = const Value.absent(), + this.recoveryContactsThreshold = const Value.absent(), + this.askForFriendPromotions = const Value.absent(), + this.mediaSendCounter = const Value.absent(), + this.mediaReceivedCounter = const Value.absent(), + }) : username = Value(username); + static Insertable custom({ + Expression? userId, + Expression? username, + Expression? displayName, + Expression? nickName, + Expression? avatarSvgCompressed, + Expression? senderProfileCounter, + Expression? accepted, + Expression? deletedByUser, + Expression? requested, + Expression? blocked, + Expression? verified, + Expression? accountDeleted, + Expression? createdAt, + Expression? userDiscoveryVersion, + Expression? userDiscoveryExcluded, + Expression? userDiscoveryManualApproved, + Expression? recoveryIsTrustedFriend, + Expression? recoveryLastHeartbeat, + Expression? recoverySecretShare, + Expression? recoveryContactsSecretShare, + Expression? recoveryContactsLastHeartbeat, + Expression? recoveryContactsThreshold, + Expression? askForFriendPromotions, + Expression? mediaSendCounter, + Expression? mediaReceivedCounter, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (username != null) 'username': username, + if (displayName != null) 'display_name': displayName, + if (nickName != null) 'nick_name': nickName, + if (avatarSvgCompressed != null) + 'avatar_svg_compressed': avatarSvgCompressed, + if (senderProfileCounter != null) + 'sender_profile_counter': senderProfileCounter, + if (accepted != null) 'accepted': accepted, + if (deletedByUser != null) 'deleted_by_user': deletedByUser, + if (requested != null) 'requested': requested, + if (blocked != null) 'blocked': blocked, + if (verified != null) 'verified': verified, + if (accountDeleted != null) 'account_deleted': accountDeleted, + if (createdAt != null) 'created_at': createdAt, + if (userDiscoveryVersion != null) + 'user_discovery_version': userDiscoveryVersion, + if (userDiscoveryExcluded != null) + 'user_discovery_excluded': userDiscoveryExcluded, + if (userDiscoveryManualApproved != null) + 'user_discovery_manual_approved': userDiscoveryManualApproved, + if (recoveryIsTrustedFriend != null) + 'recovery_is_trusted_friend': recoveryIsTrustedFriend, + if (recoveryLastHeartbeat != null) + 'recovery_last_heartbeat': recoveryLastHeartbeat, + if (recoverySecretShare != null) + 'recovery_secret_share': recoverySecretShare, + if (recoveryContactsSecretShare != null) + 'recovery_contacts_secret_share': recoveryContactsSecretShare, + if (recoveryContactsLastHeartbeat != null) + 'recovery_contacts_last_heartbeat': recoveryContactsLastHeartbeat, + if (recoveryContactsThreshold != null) + 'recovery_contacts_threshold': recoveryContactsThreshold, + if (askForFriendPromotions != null) + 'ask_for_friend_promotions': askForFriendPromotions, + if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, + if (mediaReceivedCounter != null) + 'media_received_counter': mediaReceivedCounter, + }); + } + + ContactsCompanion copyWith({ + Value? userId, + Value? username, + Value? displayName, + Value? nickName, + Value? avatarSvgCompressed, + Value? senderProfileCounter, + Value? accepted, + Value? deletedByUser, + Value? requested, + Value? blocked, + Value? verified, + Value? accountDeleted, + Value? createdAt, + Value? userDiscoveryVersion, + Value? userDiscoveryExcluded, + Value? userDiscoveryManualApproved, + Value? recoveryIsTrustedFriend, + Value? recoveryLastHeartbeat, + Value? recoverySecretShare, + Value? recoveryContactsSecretShare, + Value? recoveryContactsLastHeartbeat, + Value? recoveryContactsThreshold, + Value? askForFriendPromotions, + Value? mediaSendCounter, + Value? mediaReceivedCounter, + }) { + return ContactsCompanion( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName ?? this.displayName, + nickName: nickName ?? this.nickName, + avatarSvgCompressed: avatarSvgCompressed ?? this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + userDiscoveryVersion: userDiscoveryVersion ?? this.userDiscoveryVersion, + userDiscoveryExcluded: + userDiscoveryExcluded ?? this.userDiscoveryExcluded, + userDiscoveryManualApproved: + userDiscoveryManualApproved ?? this.userDiscoveryManualApproved, + recoveryIsTrustedFriend: + recoveryIsTrustedFriend ?? this.recoveryIsTrustedFriend, + recoveryLastHeartbeat: + recoveryLastHeartbeat ?? this.recoveryLastHeartbeat, + recoverySecretShare: recoverySecretShare ?? this.recoverySecretShare, + recoveryContactsSecretShare: + recoveryContactsSecretShare ?? this.recoveryContactsSecretShare, + recoveryContactsLastHeartbeat: + recoveryContactsLastHeartbeat ?? this.recoveryContactsLastHeartbeat, + recoveryContactsThreshold: + recoveryContactsThreshold ?? this.recoveryContactsThreshold, + askForFriendPromotions: + askForFriendPromotions ?? this.askForFriendPromotions, + mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, + mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (nickName.present) { + map['nick_name'] = Variable(nickName.value); + } + if (avatarSvgCompressed.present) { + map['avatar_svg_compressed'] = Variable( + avatarSvgCompressed.value, + ); + } + if (senderProfileCounter.present) { + map['sender_profile_counter'] = Variable(senderProfileCounter.value); + } + if (accepted.present) { + map['accepted'] = Variable(accepted.value); + } + if (deletedByUser.present) { + map['deleted_by_user'] = Variable(deletedByUser.value); + } + if (requested.present) { + map['requested'] = Variable(requested.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (accountDeleted.present) { + map['account_deleted'] = Variable(accountDeleted.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (userDiscoveryVersion.present) { + map['user_discovery_version'] = Variable( + userDiscoveryVersion.value, + ); + } + if (userDiscoveryExcluded.present) { + map['user_discovery_excluded'] = Variable( + userDiscoveryExcluded.value, + ); + } + if (userDiscoveryManualApproved.present) { + map['user_discovery_manual_approved'] = Variable( + userDiscoveryManualApproved.value, + ); + } + if (recoveryIsTrustedFriend.present) { + map['recovery_is_trusted_friend'] = Variable( + recoveryIsTrustedFriend.value, + ); + } + if (recoveryLastHeartbeat.present) { + map['recovery_last_heartbeat'] = Variable( + recoveryLastHeartbeat.value, + ); + } + if (recoverySecretShare.present) { + map['recovery_secret_share'] = Variable( + recoverySecretShare.value, + ); + } + if (recoveryContactsSecretShare.present) { + map['recovery_contacts_secret_share'] = Variable( + recoveryContactsSecretShare.value, + ); + } + if (recoveryContactsLastHeartbeat.present) { + map['recovery_contacts_last_heartbeat'] = Variable( + recoveryContactsLastHeartbeat.value, + ); + } + if (recoveryContactsThreshold.present) { + map['recovery_contacts_threshold'] = Variable( + recoveryContactsThreshold.value, + ); + } + if (askForFriendPromotions.present) { + map['ask_for_friend_promotions'] = Variable( + askForFriendPromotions.value, + ); + } + if (mediaSendCounter.present) { + map['media_send_counter'] = Variable(mediaSendCounter.value); + } + if (mediaReceivedCounter.present) { + map['media_received_counter'] = Variable(mediaReceivedCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContactsCompanion(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt, ') + ..write('userDiscoveryVersion: $userDiscoveryVersion, ') + ..write('userDiscoveryExcluded: $userDiscoveryExcluded, ') + ..write('userDiscoveryManualApproved: $userDiscoveryManualApproved, ') + ..write('recoveryIsTrustedFriend: $recoveryIsTrustedFriend, ') + ..write('recoveryLastHeartbeat: $recoveryLastHeartbeat, ') + ..write('recoverySecretShare: $recoverySecretShare, ') + ..write('recoveryContactsSecretShare: $recoveryContactsSecretShare, ') + ..write( + 'recoveryContactsLastHeartbeat: $recoveryContactsLastHeartbeat, ', + ) + ..write('recoveryContactsThreshold: $recoveryContactsThreshold, ') + ..write('askForFriendPromotions: $askForFriendPromotions, ') + ..write('mediaSendCounter: $mediaSendCounter, ') + ..write('mediaReceivedCounter: $mediaReceivedCounter') + ..write(')')) + .toString(); + } +} + +class Groups extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Groups(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isGroupAdmin = GeneratedColumn( + 'is_group_admin', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_group_admin IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isDirectChat = GeneratedColumn( + 'is_direct_chat', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_direct_chat IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn archived = GeneratedColumn( + 'archived', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (archived IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn joinedGroup = GeneratedColumn( + 'joined_group', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (joined_group IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn leftGroup = GeneratedColumn( + 'left_group', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (left_group IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn deletedContent = GeneratedColumn( + 'deleted_content', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (deleted_content IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn stateVersionId = GeneratedColumn( + 'state_version_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn stateEncryptionKey = + GeneratedColumn( + 'state_encryption_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn myGroupPrivateKey = + GeneratedColumn( + 'my_group_private_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn groupName = GeneratedColumn( + 'group_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn draftMessage = GeneratedColumn( + 'draft_message', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn totalMediaCounter = GeneratedColumn( + 'total_media_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn alsoBestFriend = GeneratedColumn( + 'also_best_friend', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (also_best_friend IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn deleteMessagesAfterMilliseconds = + GeneratedColumn( + 'delete_messages_after_milliseconds', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 86400000', + defaultValue: const CustomExpression('86400000'), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn lastMessageSend = GeneratedColumn( + 'last_message_send', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastMessageReceived = GeneratedColumn( + 'last_message_received', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastFlameCounterChange = GeneratedColumn( + 'last_flame_counter_change', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastFlameSync = GeneratedColumn( + 'last_flame_sync', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn flameCounter = GeneratedColumn( + 'flame_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn maxFlameCounter = GeneratedColumn( + 'max_flame_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn maxFlameCounterFrom = GeneratedColumn( + 'max_flame_counter_from', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastMessageExchange = GeneratedColumn( + 'last_message_exchange', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + stateEncryptionKey, + myGroupPrivateKey, + groupName, + draftMessage, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + Set get $primaryKey => {groupId}; + @override + GroupsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupsData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + isGroupAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_group_admin'], + )!, + isDirectChat: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_direct_chat'], + )!, + pinned: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pinned'], + )!, + archived: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}archived'], + )!, + joinedGroup: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}joined_group'], + )!, + leftGroup: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}left_group'], + )!, + deletedContent: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}deleted_content'], + )!, + stateVersionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}state_version_id'], + )!, + stateEncryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}state_encryption_key'], + ), + myGroupPrivateKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}my_group_private_key'], + ), + groupName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_name'], + )!, + draftMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}draft_message'], + ), + totalMediaCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}total_media_counter'], + )!, + alsoBestFriend: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}also_best_friend'], + )!, + deleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}delete_messages_after_milliseconds'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + lastMessageSend: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message_send'], + ), + lastMessageReceived: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message_received'], + ), + lastFlameCounterChange: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_flame_counter_change'], + ), + lastFlameSync: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_flame_sync'], + ), + flameCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}flame_counter'], + )!, + maxFlameCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}max_flame_counter'], + )!, + maxFlameCounterFrom: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}max_flame_counter_from'], + ), + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message_exchange'], + )!, + ); + } + + @override + Groups createAlias(String alias) { + return Groups(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(group_id)']; + @override + bool get dontWriteConstraints => true; +} + +class GroupsData extends DataClass implements Insertable { + final String groupId; + final int isGroupAdmin; + final int isDirectChat; + final int pinned; + final int archived; + final int joinedGroup; + final int leftGroup; + final int deletedContent; + final int stateVersionId; + final i2.Uint8List? stateEncryptionKey; + final i2.Uint8List? myGroupPrivateKey; + final String groupName; + final String? draftMessage; + final int totalMediaCounter; + final int alsoBestFriend; + final int deleteMessagesAfterMilliseconds; + final int createdAt; + final int? lastMessageSend; + final int? lastMessageReceived; + final int? lastFlameCounterChange; + final int? lastFlameSync; + final int flameCounter; + final int maxFlameCounter; + final int? maxFlameCounterFrom; + final int lastMessageExchange; + const GroupsData({ + required this.groupId, + required this.isGroupAdmin, + required this.isDirectChat, + required this.pinned, + required this.archived, + required this.joinedGroup, + required this.leftGroup, + required this.deletedContent, + required this.stateVersionId, + this.stateEncryptionKey, + this.myGroupPrivateKey, + required this.groupName, + this.draftMessage, + required this.totalMediaCounter, + required this.alsoBestFriend, + required this.deleteMessagesAfterMilliseconds, + required this.createdAt, + this.lastMessageSend, + this.lastMessageReceived, + this.lastFlameCounterChange, + this.lastFlameSync, + required this.flameCounter, + required this.maxFlameCounter, + this.maxFlameCounterFrom, + required this.lastMessageExchange, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['is_group_admin'] = Variable(isGroupAdmin); + map['is_direct_chat'] = Variable(isDirectChat); + map['pinned'] = Variable(pinned); + map['archived'] = Variable(archived); + map['joined_group'] = Variable(joinedGroup); + map['left_group'] = Variable(leftGroup); + map['deleted_content'] = Variable(deletedContent); + map['state_version_id'] = Variable(stateVersionId); + if (!nullToAbsent || stateEncryptionKey != null) { + map['state_encryption_key'] = Variable(stateEncryptionKey); + } + if (!nullToAbsent || myGroupPrivateKey != null) { + map['my_group_private_key'] = Variable(myGroupPrivateKey); + } + map['group_name'] = Variable(groupName); + if (!nullToAbsent || draftMessage != null) { + map['draft_message'] = Variable(draftMessage); + } + map['total_media_counter'] = Variable(totalMediaCounter); + map['also_best_friend'] = Variable(alsoBestFriend); + map['delete_messages_after_milliseconds'] = Variable( + deleteMessagesAfterMilliseconds, + ); + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || lastMessageSend != null) { + map['last_message_send'] = Variable(lastMessageSend); + } + if (!nullToAbsent || lastMessageReceived != null) { + map['last_message_received'] = Variable(lastMessageReceived); + } + if (!nullToAbsent || lastFlameCounterChange != null) { + map['last_flame_counter_change'] = Variable(lastFlameCounterChange); + } + if (!nullToAbsent || lastFlameSync != null) { + map['last_flame_sync'] = Variable(lastFlameSync); + } + map['flame_counter'] = Variable(flameCounter); + map['max_flame_counter'] = Variable(maxFlameCounter); + if (!nullToAbsent || maxFlameCounterFrom != null) { + map['max_flame_counter_from'] = Variable(maxFlameCounterFrom); + } + map['last_message_exchange'] = Variable(lastMessageExchange); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isDirectChat: Value(isDirectChat), + pinned: Value(pinned), + archived: Value(archived), + joinedGroup: Value(joinedGroup), + leftGroup: Value(leftGroup), + deletedContent: Value(deletedContent), + stateVersionId: Value(stateVersionId), + stateEncryptionKey: stateEncryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(stateEncryptionKey), + myGroupPrivateKey: myGroupPrivateKey == null && nullToAbsent + ? const Value.absent() + : Value(myGroupPrivateKey), + groupName: Value(groupName), + draftMessage: draftMessage == null && nullToAbsent + ? const Value.absent() + : Value(draftMessage), + totalMediaCounter: Value(totalMediaCounter), + alsoBestFriend: Value(alsoBestFriend), + deleteMessagesAfterMilliseconds: Value(deleteMessagesAfterMilliseconds), + createdAt: Value(createdAt), + lastMessageSend: lastMessageSend == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageSend), + lastMessageReceived: lastMessageReceived == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageReceived), + lastFlameCounterChange: lastFlameCounterChange == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameCounterChange), + lastFlameSync: lastFlameSync == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameSync), + flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: maxFlameCounterFrom == null && nullToAbsent + ? const Value.absent() + : Value(maxFlameCounterFrom), + lastMessageExchange: Value(lastMessageExchange), + ); + } + + factory GroupsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupsData( + groupId: serializer.fromJson(json['groupId']), + isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), + isDirectChat: serializer.fromJson(json['isDirectChat']), + pinned: serializer.fromJson(json['pinned']), + archived: serializer.fromJson(json['archived']), + joinedGroup: serializer.fromJson(json['joinedGroup']), + leftGroup: serializer.fromJson(json['leftGroup']), + deletedContent: serializer.fromJson(json['deletedContent']), + stateVersionId: serializer.fromJson(json['stateVersionId']), + stateEncryptionKey: serializer.fromJson( + json['stateEncryptionKey'], + ), + myGroupPrivateKey: serializer.fromJson( + json['myGroupPrivateKey'], + ), + groupName: serializer.fromJson(json['groupName']), + draftMessage: serializer.fromJson(json['draftMessage']), + totalMediaCounter: serializer.fromJson(json['totalMediaCounter']), + alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), + deleteMessagesAfterMilliseconds: serializer.fromJson( + json['deleteMessagesAfterMilliseconds'], + ), + createdAt: serializer.fromJson(json['createdAt']), + lastMessageSend: serializer.fromJson(json['lastMessageSend']), + lastMessageReceived: serializer.fromJson( + json['lastMessageReceived'], + ), + lastFlameCounterChange: serializer.fromJson( + json['lastFlameCounterChange'], + ), + lastFlameSync: serializer.fromJson(json['lastFlameSync']), + flameCounter: serializer.fromJson(json['flameCounter']), + maxFlameCounter: serializer.fromJson(json['maxFlameCounter']), + maxFlameCounterFrom: serializer.fromJson( + json['maxFlameCounterFrom'], + ), + lastMessageExchange: serializer.fromJson( + json['lastMessageExchange'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'isGroupAdmin': serializer.toJson(isGroupAdmin), + 'isDirectChat': serializer.toJson(isDirectChat), + 'pinned': serializer.toJson(pinned), + 'archived': serializer.toJson(archived), + 'joinedGroup': serializer.toJson(joinedGroup), + 'leftGroup': serializer.toJson(leftGroup), + 'deletedContent': serializer.toJson(deletedContent), + 'stateVersionId': serializer.toJson(stateVersionId), + 'stateEncryptionKey': serializer.toJson( + stateEncryptionKey, + ), + 'myGroupPrivateKey': serializer.toJson(myGroupPrivateKey), + 'groupName': serializer.toJson(groupName), + 'draftMessage': serializer.toJson(draftMessage), + 'totalMediaCounter': serializer.toJson(totalMediaCounter), + 'alsoBestFriend': serializer.toJson(alsoBestFriend), + 'deleteMessagesAfterMilliseconds': serializer.toJson( + deleteMessagesAfterMilliseconds, + ), + 'createdAt': serializer.toJson(createdAt), + 'lastMessageSend': serializer.toJson(lastMessageSend), + 'lastMessageReceived': serializer.toJson(lastMessageReceived), + 'lastFlameCounterChange': serializer.toJson(lastFlameCounterChange), + 'lastFlameSync': serializer.toJson(lastFlameSync), + 'flameCounter': serializer.toJson(flameCounter), + 'maxFlameCounter': serializer.toJson(maxFlameCounter), + 'maxFlameCounterFrom': serializer.toJson(maxFlameCounterFrom), + 'lastMessageExchange': serializer.toJson(lastMessageExchange), + }; + } + + GroupsData copyWith({ + String? groupId, + int? isGroupAdmin, + int? isDirectChat, + int? pinned, + int? archived, + int? joinedGroup, + int? leftGroup, + int? deletedContent, + int? stateVersionId, + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), + String? groupName, + Value draftMessage = const Value.absent(), + int? totalMediaCounter, + int? alsoBestFriend, + int? deleteMessagesAfterMilliseconds, + int? createdAt, + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + int? flameCounter, + int? maxFlameCounter, + Value maxFlameCounterFrom = const Value.absent(), + int? lastMessageExchange, + }) => GroupsData( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey.present + ? stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey.present + ? myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + draftMessage: draftMessage.present ? draftMessage.value : this.draftMessage, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + deleteMessagesAfterMilliseconds ?? this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend.present + ? lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: lastMessageReceived.present + ? lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange.present + ? lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: lastFlameSync.present + ? lastFlameSync.value + : this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom.present + ? maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + ); + GroupsData copyWithCompanion(GroupsCompanion data) { + return GroupsData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isDirectChat: data.isDirectChat.present + ? data.isDirectChat.value + : this.isDirectChat, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, + joinedGroup: data.joinedGroup.present + ? data.joinedGroup.value + : this.joinedGroup, + leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, + deletedContent: data.deletedContent.present + ? data.deletedContent.value + : this.deletedContent, + stateVersionId: data.stateVersionId.present + ? data.stateVersionId.value + : this.stateVersionId, + stateEncryptionKey: data.stateEncryptionKey.present + ? data.stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: data.myGroupPrivateKey.present + ? data.myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: data.groupName.present ? data.groupName.value : this.groupName, + draftMessage: data.draftMessage.present + ? data.draftMessage.value + : this.draftMessage, + totalMediaCounter: data.totalMediaCounter.present + ? data.totalMediaCounter.value + : this.totalMediaCounter, + alsoBestFriend: data.alsoBestFriend.present + ? data.alsoBestFriend.value + : this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + data.deleteMessagesAfterMilliseconds.present + ? data.deleteMessagesAfterMilliseconds.value + : this.deleteMessagesAfterMilliseconds, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastMessageSend: data.lastMessageSend.present + ? data.lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: data.lastMessageReceived.present + ? data.lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: data.lastFlameCounterChange.present + ? data.lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: data.lastFlameSync.present + ? data.lastFlameSync.value + : this.lastFlameSync, + flameCounter: data.flameCounter.present + ? data.flameCounter.value + : this.flameCounter, + maxFlameCounter: data.maxFlameCounter.present + ? data.maxFlameCounter.value + : this.maxFlameCounter, + maxFlameCounterFrom: data.maxFlameCounterFrom.present + ? data.maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, + ); + } + + @override + String toString() { + return (StringBuffer('GroupsData(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('draftMessage: $draftMessage, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ', + ) + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + $driftBlobEquality.hash(stateEncryptionKey), + $driftBlobEquality.hash(myGroupPrivateKey), + groupName, + draftMessage, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupsData && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isDirectChat == this.isDirectChat && + other.pinned == this.pinned && + other.archived == this.archived && + other.joinedGroup == this.joinedGroup && + other.leftGroup == this.leftGroup && + other.deletedContent == this.deletedContent && + other.stateVersionId == this.stateVersionId && + $driftBlobEquality.equals( + other.stateEncryptionKey, + this.stateEncryptionKey, + ) && + $driftBlobEquality.equals( + other.myGroupPrivateKey, + this.myGroupPrivateKey, + ) && + other.groupName == this.groupName && + other.draftMessage == this.draftMessage && + other.totalMediaCounter == this.totalMediaCounter && + other.alsoBestFriend == this.alsoBestFriend && + other.deleteMessagesAfterMilliseconds == + this.deleteMessagesAfterMilliseconds && + other.createdAt == this.createdAt && + other.lastMessageSend == this.lastMessageSend && + other.lastMessageReceived == this.lastMessageReceived && + other.lastFlameCounterChange == this.lastFlameCounterChange && + other.lastFlameSync == this.lastFlameSync && + other.flameCounter == this.flameCounter && + other.maxFlameCounter == this.maxFlameCounter && + other.maxFlameCounterFrom == this.maxFlameCounterFrom && + other.lastMessageExchange == this.lastMessageExchange); +} + +class GroupsCompanion extends UpdateCompanion { + final Value groupId; + final Value isGroupAdmin; + final Value isDirectChat; + final Value pinned; + final Value archived; + final Value joinedGroup; + final Value leftGroup; + final Value deletedContent; + final Value stateVersionId; + final Value stateEncryptionKey; + final Value myGroupPrivateKey; + final Value groupName; + final Value draftMessage; + final Value totalMediaCounter; + final Value alsoBestFriend; + final Value deleteMessagesAfterMilliseconds; + final Value createdAt; + final Value lastMessageSend; + final Value lastMessageReceived; + final Value lastFlameCounterChange; + final Value lastFlameSync; + final Value flameCounter; + final Value maxFlameCounter; + final Value maxFlameCounterFrom; + final Value lastMessageExchange; + final Value rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + this.groupName = const Value.absent(), + this.draftMessage = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + required String groupId, + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + required String groupName, + this.draftMessage = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + groupName = Value(groupName); + static Insertable custom({ + Expression? groupId, + Expression? isGroupAdmin, + Expression? isDirectChat, + Expression? pinned, + Expression? archived, + Expression? joinedGroup, + Expression? leftGroup, + Expression? deletedContent, + Expression? stateVersionId, + Expression? stateEncryptionKey, + Expression? myGroupPrivateKey, + Expression? groupName, + Expression? draftMessage, + Expression? totalMediaCounter, + Expression? alsoBestFriend, + Expression? deleteMessagesAfterMilliseconds, + Expression? createdAt, + Expression? lastMessageSend, + Expression? lastMessageReceived, + Expression? lastFlameCounterChange, + Expression? lastFlameSync, + Expression? flameCounter, + Expression? maxFlameCounter, + Expression? maxFlameCounterFrom, + Expression? lastMessageExchange, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isDirectChat != null) 'is_direct_chat': isDirectChat, + if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, + if (joinedGroup != null) 'joined_group': joinedGroup, + if (leftGroup != null) 'left_group': leftGroup, + if (deletedContent != null) 'deleted_content': deletedContent, + if (stateVersionId != null) 'state_version_id': stateVersionId, + if (stateEncryptionKey != null) + 'state_encryption_key': stateEncryptionKey, + if (myGroupPrivateKey != null) 'my_group_private_key': myGroupPrivateKey, + if (groupName != null) 'group_name': groupName, + if (draftMessage != null) 'draft_message': draftMessage, + if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, + if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, + if (deleteMessagesAfterMilliseconds != null) + 'delete_messages_after_milliseconds': deleteMessagesAfterMilliseconds, + if (createdAt != null) 'created_at': createdAt, + if (lastMessageSend != null) 'last_message_send': lastMessageSend, + if (lastMessageReceived != null) + 'last_message_received': lastMessageReceived, + if (lastFlameCounterChange != null) + 'last_flame_counter_change': lastFlameCounterChange, + if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, + if (flameCounter != null) 'flame_counter': flameCounter, + if (maxFlameCounter != null) 'max_flame_counter': maxFlameCounter, + if (maxFlameCounterFrom != null) + 'max_flame_counter_from': maxFlameCounterFrom, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith({ + Value? groupId, + Value? isGroupAdmin, + Value? isDirectChat, + Value? pinned, + Value? archived, + Value? joinedGroup, + Value? leftGroup, + Value? deletedContent, + Value? stateVersionId, + Value? stateEncryptionKey, + Value? myGroupPrivateKey, + Value? groupName, + Value? draftMessage, + Value? totalMediaCounter, + Value? alsoBestFriend, + Value? deleteMessagesAfterMilliseconds, + Value? createdAt, + Value? lastMessageSend, + Value? lastMessageReceived, + Value? lastFlameCounterChange, + Value? lastFlameSync, + Value? flameCounter, + Value? maxFlameCounter, + Value? maxFlameCounterFrom, + Value? lastMessageExchange, + Value? rowid, + }) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + draftMessage: draftMessage ?? this.draftMessage, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend ?? this.lastMessageSend, + lastMessageReceived: lastMessageReceived ?? this.lastMessageReceived, + lastFlameCounterChange: + lastFlameCounterChange ?? this.lastFlameCounterChange, + lastFlameSync: lastFlameSync ?? this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom ?? this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable(isGroupAdmin.value); + } + if (isDirectChat.present) { + map['is_direct_chat'] = Variable(isDirectChat.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (archived.present) { + map['archived'] = Variable(archived.value); + } + if (joinedGroup.present) { + map['joined_group'] = Variable(joinedGroup.value); + } + if (leftGroup.present) { + map['left_group'] = Variable(leftGroup.value); + } + if (deletedContent.present) { + map['deleted_content'] = Variable(deletedContent.value); + } + if (stateVersionId.present) { + map['state_version_id'] = Variable(stateVersionId.value); + } + if (stateEncryptionKey.present) { + map['state_encryption_key'] = Variable( + stateEncryptionKey.value, + ); + } + if (myGroupPrivateKey.present) { + map['my_group_private_key'] = Variable( + myGroupPrivateKey.value, + ); + } + if (groupName.present) { + map['group_name'] = Variable(groupName.value); + } + if (draftMessage.present) { + map['draft_message'] = Variable(draftMessage.value); + } + if (totalMediaCounter.present) { + map['total_media_counter'] = Variable(totalMediaCounter.value); + } + if (alsoBestFriend.present) { + map['also_best_friend'] = Variable(alsoBestFriend.value); + } + if (deleteMessagesAfterMilliseconds.present) { + map['delete_messages_after_milliseconds'] = Variable( + deleteMessagesAfterMilliseconds.value, + ); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (lastMessageSend.present) { + map['last_message_send'] = Variable(lastMessageSend.value); + } + if (lastMessageReceived.present) { + map['last_message_received'] = Variable(lastMessageReceived.value); + } + if (lastFlameCounterChange.present) { + map['last_flame_counter_change'] = Variable( + lastFlameCounterChange.value, + ); + } + if (lastFlameSync.present) { + map['last_flame_sync'] = Variable(lastFlameSync.value); + } + if (flameCounter.present) { + map['flame_counter'] = Variable(flameCounter.value); + } + if (maxFlameCounter.present) { + map['max_flame_counter'] = Variable(maxFlameCounter.value); + } + if (maxFlameCounterFrom.present) { + map['max_flame_counter_from'] = Variable(maxFlameCounterFrom.value); + } + if (lastMessageExchange.present) { + map['last_message_exchange'] = Variable(lastMessageExchange.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('draftMessage: $draftMessage, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ', + ) + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MediaFiles extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MediaFiles(this.attachedDatabase, [this._alias]); + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn uploadState = GeneratedColumn( + 'upload_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn cloudState = GeneratedColumn( + 'cloud_state', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT \'none\'', + defaultValue: const CustomExpression('\'none\''), + ); + late final GeneratedColumn blurhash = GeneratedColumn( + 'blurhash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn downloadState = GeneratedColumn( + 'download_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn requiresAuthentication = GeneratedColumn( + 'requires_authentication', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (requires_authentication IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn stored = GeneratedColumn( + 'stored', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK ("stored" IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isDraftMedia = GeneratedColumn( + 'is_draft_media', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_draft_media IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasCropAnalyzed = GeneratedColumn( + 'has_crop_analyzed', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (has_crop_analyzed IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn preProgressingProcess = GeneratedColumn( + 'pre_progressing_process', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn reuploadRequestedBy = + GeneratedColumn( + 'reupload_requested_by', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn displayLimitInMilliseconds = + GeneratedColumn( + 'display_limit_in_milliseconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn removeAudio = GeneratedColumn( + 'remove_audio', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL CHECK (remove_audio IN (0, 1))', + ); + late final GeneratedColumn downloadToken = + GeneratedColumn( + 'download_token', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn encryptionKey = + GeneratedColumn( + 'encryption_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn encryptionMac = + GeneratedColumn( + 'encryption_mac', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn encryptionNonce = + GeneratedColumn( + 'encryption_nonce', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn storedFileHash = + GeneratedColumn( + 'stored_file_hash', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn hasThumbnail = GeneratedColumn( + 'has_thumbnail', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (has_thumbnail IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn sizeInBytes = GeneratedColumn( + 'size_in_bytes', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn createdAtMonth = GeneratedColumn( + 'created_at_month', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + mediaId, + type, + uploadState, + cloudState, + blurhash, + downloadState, + requiresAuthentication, + stored, + isDraftMedia, + isFavorite, + hasCropAnalyzed, + preProgressingProcess, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + downloadToken, + encryptionKey, + encryptionMac, + encryptionNonce, + storedFileHash, + hasThumbnail, + sizeInBytes, + createdAt, + createdAtMonth, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'media_files'; + @override + Set get $primaryKey => {mediaId}; + @override + MediaFilesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MediaFilesData( + mediaId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}media_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + uploadState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}upload_state'], + ), + cloudState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_state'], + )!, + blurhash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}blurhash'], + ), + downloadState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}download_state'], + ), + requiresAuthentication: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}requires_authentication'], + )!, + stored: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}stored'], + )!, + isDraftMedia: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_draft_media'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_favorite'], + )!, + hasCropAnalyzed: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_crop_analyzed'], + )!, + preProgressingProcess: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pre_progressing_process'], + ), + reuploadRequestedBy: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reupload_requested_by'], + ), + displayLimitInMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}display_limit_in_milliseconds'], + ), + removeAudio: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}remove_audio'], + ), + downloadToken: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}download_token'], + ), + encryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}encryption_key'], + ), + encryptionMac: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}encryption_mac'], + ), + encryptionNonce: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}encryption_nonce'], + ), + storedFileHash: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}stored_file_hash'], + ), + hasThumbnail: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}has_thumbnail'], + )!, + sizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}size_in_bytes'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + createdAtMonth: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at_month'], + ), + ); + } + + @override + MediaFiles createAlias(String alias) { + return MediaFiles(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(media_id)']; + @override + bool get dontWriteConstraints => true; +} + +class MediaFilesData extends DataClass implements Insertable { + final String mediaId; + final String type; + final String? uploadState; + final String cloudState; + final String? blurhash; + final String? downloadState; + final int requiresAuthentication; + final int stored; + final int isDraftMedia; + final int isFavorite; + final int hasCropAnalyzed; + final int? preProgressingProcess; + final String? reuploadRequestedBy; + final int? displayLimitInMilliseconds; + final int? removeAudio; + final i2.Uint8List? downloadToken; + final i2.Uint8List? encryptionKey; + final i2.Uint8List? encryptionMac; + final i2.Uint8List? encryptionNonce; + final i2.Uint8List? storedFileHash; + final int hasThumbnail; + final int? sizeInBytes; + final int createdAt; + final String? createdAtMonth; + const MediaFilesData({ + required this.mediaId, + required this.type, + this.uploadState, + required this.cloudState, + this.blurhash, + this.downloadState, + required this.requiresAuthentication, + required this.stored, + required this.isDraftMedia, + required this.isFavorite, + required this.hasCropAnalyzed, + this.preProgressingProcess, + this.reuploadRequestedBy, + this.displayLimitInMilliseconds, + this.removeAudio, + this.downloadToken, + this.encryptionKey, + this.encryptionMac, + this.encryptionNonce, + this.storedFileHash, + required this.hasThumbnail, + this.sizeInBytes, + required this.createdAt, + this.createdAtMonth, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['media_id'] = Variable(mediaId); + map['type'] = Variable(type); + if (!nullToAbsent || uploadState != null) { + map['upload_state'] = Variable(uploadState); + } + map['cloud_state'] = Variable(cloudState); + if (!nullToAbsent || blurhash != null) { + map['blurhash'] = Variable(blurhash); + } + if (!nullToAbsent || downloadState != null) { + map['download_state'] = Variable(downloadState); + } + map['requires_authentication'] = Variable(requiresAuthentication); + map['stored'] = Variable(stored); + map['is_draft_media'] = Variable(isDraftMedia); + map['is_favorite'] = Variable(isFavorite); + map['has_crop_analyzed'] = Variable(hasCropAnalyzed); + if (!nullToAbsent || preProgressingProcess != null) { + map['pre_progressing_process'] = Variable(preProgressingProcess); + } + if (!nullToAbsent || reuploadRequestedBy != null) { + map['reupload_requested_by'] = Variable(reuploadRequestedBy); + } + if (!nullToAbsent || displayLimitInMilliseconds != null) { + map['display_limit_in_milliseconds'] = Variable( + displayLimitInMilliseconds, + ); + } + if (!nullToAbsent || removeAudio != null) { + map['remove_audio'] = Variable(removeAudio); + } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || encryptionKey != null) { + map['encryption_key'] = Variable(encryptionKey); + } + if (!nullToAbsent || encryptionMac != null) { + map['encryption_mac'] = Variable(encryptionMac); + } + if (!nullToAbsent || encryptionNonce != null) { + map['encryption_nonce'] = Variable(encryptionNonce); + } + if (!nullToAbsent || storedFileHash != null) { + map['stored_file_hash'] = Variable(storedFileHash); + } + map['has_thumbnail'] = Variable(hasThumbnail); + if (!nullToAbsent || sizeInBytes != null) { + map['size_in_bytes'] = Variable(sizeInBytes); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || createdAtMonth != null) { + map['created_at_month'] = Variable(createdAtMonth); + } + return map; + } + + MediaFilesCompanion toCompanion(bool nullToAbsent) { + return MediaFilesCompanion( + mediaId: Value(mediaId), + type: Value(type), + uploadState: uploadState == null && nullToAbsent + ? const Value.absent() + : Value(uploadState), + cloudState: Value(cloudState), + blurhash: blurhash == null && nullToAbsent + ? const Value.absent() + : Value(blurhash), + downloadState: downloadState == null && nullToAbsent + ? const Value.absent() + : Value(downloadState), + requiresAuthentication: Value(requiresAuthentication), + stored: Value(stored), + isDraftMedia: Value(isDraftMedia), + isFavorite: Value(isFavorite), + hasCropAnalyzed: Value(hasCropAnalyzed), + preProgressingProcess: preProgressingProcess == null && nullToAbsent + ? const Value.absent() + : Value(preProgressingProcess), + reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent + ? const Value.absent() + : Value(reuploadRequestedBy), + displayLimitInMilliseconds: + displayLimitInMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(displayLimitInMilliseconds), + removeAudio: removeAudio == null && nullToAbsent + ? const Value.absent() + : Value(removeAudio), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + encryptionKey: encryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(encryptionKey), + encryptionMac: encryptionMac == null && nullToAbsent + ? const Value.absent() + : Value(encryptionMac), + encryptionNonce: encryptionNonce == null && nullToAbsent + ? const Value.absent() + : Value(encryptionNonce), + storedFileHash: storedFileHash == null && nullToAbsent + ? const Value.absent() + : Value(storedFileHash), + hasThumbnail: Value(hasThumbnail), + sizeInBytes: sizeInBytes == null && nullToAbsent + ? const Value.absent() + : Value(sizeInBytes), + createdAt: Value(createdAt), + createdAtMonth: createdAtMonth == null && nullToAbsent + ? const Value.absent() + : Value(createdAtMonth), + ); + } + + factory MediaFilesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MediaFilesData( + mediaId: serializer.fromJson(json['mediaId']), + type: serializer.fromJson(json['type']), + uploadState: serializer.fromJson(json['uploadState']), + cloudState: serializer.fromJson(json['cloudState']), + blurhash: serializer.fromJson(json['blurhash']), + downloadState: serializer.fromJson(json['downloadState']), + requiresAuthentication: serializer.fromJson( + json['requiresAuthentication'], + ), + stored: serializer.fromJson(json['stored']), + isDraftMedia: serializer.fromJson(json['isDraftMedia']), + isFavorite: serializer.fromJson(json['isFavorite']), + hasCropAnalyzed: serializer.fromJson(json['hasCropAnalyzed']), + preProgressingProcess: serializer.fromJson( + json['preProgressingProcess'], + ), + reuploadRequestedBy: serializer.fromJson( + json['reuploadRequestedBy'], + ), + displayLimitInMilliseconds: serializer.fromJson( + json['displayLimitInMilliseconds'], + ), + removeAudio: serializer.fromJson(json['removeAudio']), + downloadToken: serializer.fromJson(json['downloadToken']), + encryptionKey: serializer.fromJson(json['encryptionKey']), + encryptionMac: serializer.fromJson(json['encryptionMac']), + encryptionNonce: serializer.fromJson( + json['encryptionNonce'], + ), + storedFileHash: serializer.fromJson( + json['storedFileHash'], + ), + hasThumbnail: serializer.fromJson(json['hasThumbnail']), + sizeInBytes: serializer.fromJson(json['sizeInBytes']), + createdAt: serializer.fromJson(json['createdAt']), + createdAtMonth: serializer.fromJson(json['createdAtMonth']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'mediaId': serializer.toJson(mediaId), + 'type': serializer.toJson(type), + 'uploadState': serializer.toJson(uploadState), + 'cloudState': serializer.toJson(cloudState), + 'blurhash': serializer.toJson(blurhash), + 'downloadState': serializer.toJson(downloadState), + 'requiresAuthentication': serializer.toJson(requiresAuthentication), + 'stored': serializer.toJson(stored), + 'isDraftMedia': serializer.toJson(isDraftMedia), + 'isFavorite': serializer.toJson(isFavorite), + 'hasCropAnalyzed': serializer.toJson(hasCropAnalyzed), + 'preProgressingProcess': serializer.toJson(preProgressingProcess), + 'reuploadRequestedBy': serializer.toJson(reuploadRequestedBy), + 'displayLimitInMilliseconds': serializer.toJson( + displayLimitInMilliseconds, + ), + 'removeAudio': serializer.toJson(removeAudio), + 'downloadToken': serializer.toJson(downloadToken), + 'encryptionKey': serializer.toJson(encryptionKey), + 'encryptionMac': serializer.toJson(encryptionMac), + 'encryptionNonce': serializer.toJson(encryptionNonce), + 'storedFileHash': serializer.toJson(storedFileHash), + 'hasThumbnail': serializer.toJson(hasThumbnail), + 'sizeInBytes': serializer.toJson(sizeInBytes), + 'createdAt': serializer.toJson(createdAt), + 'createdAtMonth': serializer.toJson(createdAtMonth), + }; + } + + MediaFilesData copyWith({ + String? mediaId, + String? type, + Value uploadState = const Value.absent(), + String? cloudState, + Value blurhash = const Value.absent(), + Value downloadState = const Value.absent(), + int? requiresAuthentication, + int? stored, + int? isDraftMedia, + int? isFavorite, + int? hasCropAnalyzed, + Value preProgressingProcess = const Value.absent(), + Value reuploadRequestedBy = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + Value storedFileHash = const Value.absent(), + int? hasThumbnail, + Value sizeInBytes = const Value.absent(), + int? createdAt, + Value createdAtMonth = const Value.absent(), + }) => MediaFilesData( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState.present ? uploadState.value : this.uploadState, + cloudState: cloudState ?? this.cloudState, + blurhash: blurhash.present ? blurhash.value : this.blurhash, + downloadState: downloadState.present + ? downloadState.value + : this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, + preProgressingProcess: preProgressingProcess.present + ? preProgressingProcess.value + : this.preProgressingProcess, + reuploadRequestedBy: reuploadRequestedBy.present + ? reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: displayLimitInMilliseconds.present + ? displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: removeAudio.present ? removeAudio.value : this.removeAudio, + downloadToken: downloadToken.present + ? downloadToken.value + : this.downloadToken, + encryptionKey: encryptionKey.present + ? encryptionKey.value + : this.encryptionKey, + encryptionMac: encryptionMac.present + ? encryptionMac.value + : this.encryptionMac, + encryptionNonce: encryptionNonce.present + ? encryptionNonce.value + : this.encryptionNonce, + storedFileHash: storedFileHash.present + ? storedFileHash.value + : this.storedFileHash, + hasThumbnail: hasThumbnail ?? this.hasThumbnail, + sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes, + createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth.present + ? createdAtMonth.value + : this.createdAtMonth, + ); + MediaFilesData copyWithCompanion(MediaFilesCompanion data) { + return MediaFilesData( + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + type: data.type.present ? data.type.value : this.type, + uploadState: data.uploadState.present + ? data.uploadState.value + : this.uploadState, + cloudState: data.cloudState.present + ? data.cloudState.value + : this.cloudState, + blurhash: data.blurhash.present ? data.blurhash.value : this.blurhash, + downloadState: data.downloadState.present + ? data.downloadState.value + : this.downloadState, + requiresAuthentication: data.requiresAuthentication.present + ? data.requiresAuthentication.value + : this.requiresAuthentication, + stored: data.stored.present ? data.stored.value : this.stored, + isDraftMedia: data.isDraftMedia.present + ? data.isDraftMedia.value + : this.isDraftMedia, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + hasCropAnalyzed: data.hasCropAnalyzed.present + ? data.hasCropAnalyzed.value + : this.hasCropAnalyzed, + preProgressingProcess: data.preProgressingProcess.present + ? data.preProgressingProcess.value + : this.preProgressingProcess, + reuploadRequestedBy: data.reuploadRequestedBy.present + ? data.reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: data.displayLimitInMilliseconds.present + ? data.displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: data.removeAudio.present + ? data.removeAudio.value + : this.removeAudio, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + encryptionKey: data.encryptionKey.present + ? data.encryptionKey.value + : this.encryptionKey, + encryptionMac: data.encryptionMac.present + ? data.encryptionMac.value + : this.encryptionMac, + encryptionNonce: data.encryptionNonce.present + ? data.encryptionNonce.value + : this.encryptionNonce, + storedFileHash: data.storedFileHash.present + ? data.storedFileHash.value + : this.storedFileHash, + hasThumbnail: data.hasThumbnail.present + ? data.hasThumbnail.value + : this.hasThumbnail, + sizeInBytes: data.sizeInBytes.present + ? data.sizeInBytes.value + : this.sizeInBytes, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + createdAtMonth: data.createdAtMonth.present + ? data.createdAtMonth.value + : this.createdAtMonth, + ); + } + + @override + String toString() { + return (StringBuffer('MediaFilesData(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('cloudState: $cloudState, ') + ..write('blurhash: $blurhash, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') + ..write('preProgressingProcess: $preProgressingProcess, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('storedFileHash: $storedFileHash, ') + ..write('hasThumbnail: $hasThumbnail, ') + ..write('sizeInBytes: $sizeInBytes, ') + ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + mediaId, + type, + uploadState, + cloudState, + blurhash, + downloadState, + requiresAuthentication, + stored, + isDraftMedia, + isFavorite, + hasCropAnalyzed, + preProgressingProcess, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + $driftBlobEquality.hash(downloadToken), + $driftBlobEquality.hash(encryptionKey), + $driftBlobEquality.hash(encryptionMac), + $driftBlobEquality.hash(encryptionNonce), + $driftBlobEquality.hash(storedFileHash), + hasThumbnail, + sizeInBytes, + createdAt, + createdAtMonth, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MediaFilesData && + other.mediaId == this.mediaId && + other.type == this.type && + other.uploadState == this.uploadState && + other.cloudState == this.cloudState && + other.blurhash == this.blurhash && + other.downloadState == this.downloadState && + other.requiresAuthentication == this.requiresAuthentication && + other.stored == this.stored && + other.isDraftMedia == this.isDraftMedia && + other.isFavorite == this.isFavorite && + other.hasCropAnalyzed == this.hasCropAnalyzed && + other.preProgressingProcess == this.preProgressingProcess && + other.reuploadRequestedBy == this.reuploadRequestedBy && + other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + other.removeAudio == this.removeAudio && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && + $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && + $driftBlobEquality.equals( + other.encryptionNonce, + this.encryptionNonce, + ) && + $driftBlobEquality.equals( + other.storedFileHash, + this.storedFileHash, + ) && + other.hasThumbnail == this.hasThumbnail && + other.sizeInBytes == this.sizeInBytes && + other.createdAt == this.createdAt && + other.createdAtMonth == this.createdAtMonth); +} + +class MediaFilesCompanion extends UpdateCompanion { + final Value mediaId; + final Value type; + final Value uploadState; + final Value cloudState; + final Value blurhash; + final Value downloadState; + final Value requiresAuthentication; + final Value stored; + final Value isDraftMedia; + final Value isFavorite; + final Value hasCropAnalyzed; + final Value preProgressingProcess; + final Value reuploadRequestedBy; + final Value displayLimitInMilliseconds; + final Value removeAudio; + final Value downloadToken; + final Value encryptionKey; + final Value encryptionMac; + final Value encryptionNonce; + final Value storedFileHash; + final Value hasThumbnail; + final Value sizeInBytes; + final Value createdAt; + final Value createdAtMonth; + final Value rowid; + const MediaFilesCompanion({ + this.mediaId = const Value.absent(), + this.type = const Value.absent(), + this.uploadState = const Value.absent(), + this.cloudState = const Value.absent(), + this.blurhash = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), + this.preProgressingProcess = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.storedFileHash = const Value.absent(), + this.hasThumbnail = const Value.absent(), + this.sizeInBytes = const Value.absent(), + this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), + this.rowid = const Value.absent(), + }); + MediaFilesCompanion.insert({ + required String mediaId, + required String type, + this.uploadState = const Value.absent(), + this.cloudState = const Value.absent(), + this.blurhash = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.isFavorite = const Value.absent(), + this.hasCropAnalyzed = const Value.absent(), + this.preProgressingProcess = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.storedFileHash = const Value.absent(), + this.hasThumbnail = const Value.absent(), + this.sizeInBytes = const Value.absent(), + this.createdAt = const Value.absent(), + this.createdAtMonth = const Value.absent(), + this.rowid = const Value.absent(), + }) : mediaId = Value(mediaId), + type = Value(type); + static Insertable custom({ + Expression? mediaId, + Expression? type, + Expression? uploadState, + Expression? cloudState, + Expression? blurhash, + Expression? downloadState, + Expression? requiresAuthentication, + Expression? stored, + Expression? isDraftMedia, + Expression? isFavorite, + Expression? hasCropAnalyzed, + Expression? preProgressingProcess, + Expression? reuploadRequestedBy, + Expression? displayLimitInMilliseconds, + Expression? removeAudio, + Expression? downloadToken, + Expression? encryptionKey, + Expression? encryptionMac, + Expression? encryptionNonce, + Expression? storedFileHash, + Expression? hasThumbnail, + Expression? sizeInBytes, + Expression? createdAt, + Expression? createdAtMonth, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (mediaId != null) 'media_id': mediaId, + if (type != null) 'type': type, + if (uploadState != null) 'upload_state': uploadState, + if (cloudState != null) 'cloud_state': cloudState, + if (blurhash != null) 'blurhash': blurhash, + if (downloadState != null) 'download_state': downloadState, + if (requiresAuthentication != null) + 'requires_authentication': requiresAuthentication, + if (stored != null) 'stored': stored, + if (isDraftMedia != null) 'is_draft_media': isDraftMedia, + if (isFavorite != null) 'is_favorite': isFavorite, + if (hasCropAnalyzed != null) 'has_crop_analyzed': hasCropAnalyzed, + if (preProgressingProcess != null) + 'pre_progressing_process': preProgressingProcess, + if (reuploadRequestedBy != null) + 'reupload_requested_by': reuploadRequestedBy, + if (displayLimitInMilliseconds != null) + 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (removeAudio != null) 'remove_audio': removeAudio, + if (downloadToken != null) 'download_token': downloadToken, + if (encryptionKey != null) 'encryption_key': encryptionKey, + if (encryptionMac != null) 'encryption_mac': encryptionMac, + if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, + if (storedFileHash != null) 'stored_file_hash': storedFileHash, + if (hasThumbnail != null) 'has_thumbnail': hasThumbnail, + if (sizeInBytes != null) 'size_in_bytes': sizeInBytes, + if (createdAt != null) 'created_at': createdAt, + if (createdAtMonth != null) 'created_at_month': createdAtMonth, + if (rowid != null) 'rowid': rowid, + }); + } + + MediaFilesCompanion copyWith({ + Value? mediaId, + Value? type, + Value? uploadState, + Value? cloudState, + Value? blurhash, + Value? downloadState, + Value? requiresAuthentication, + Value? stored, + Value? isDraftMedia, + Value? isFavorite, + Value? hasCropAnalyzed, + Value? preProgressingProcess, + Value? reuploadRequestedBy, + Value? displayLimitInMilliseconds, + Value? removeAudio, + Value? downloadToken, + Value? encryptionKey, + Value? encryptionMac, + Value? encryptionNonce, + Value? storedFileHash, + Value? hasThumbnail, + Value? sizeInBytes, + Value? createdAt, + Value? createdAtMonth, + Value? rowid, + }) { + return MediaFilesCompanion( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState ?? this.uploadState, + cloudState: cloudState ?? this.cloudState, + blurhash: blurhash ?? this.blurhash, + downloadState: downloadState ?? this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + isFavorite: isFavorite ?? this.isFavorite, + hasCropAnalyzed: hasCropAnalyzed ?? this.hasCropAnalyzed, + preProgressingProcess: + preProgressingProcess ?? this.preProgressingProcess, + reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, + displayLimitInMilliseconds: + displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + removeAudio: removeAudio ?? this.removeAudio, + downloadToken: downloadToken ?? this.downloadToken, + encryptionKey: encryptionKey ?? this.encryptionKey, + encryptionMac: encryptionMac ?? this.encryptionMac, + encryptionNonce: encryptionNonce ?? this.encryptionNonce, + storedFileHash: storedFileHash ?? this.storedFileHash, + hasThumbnail: hasThumbnail ?? this.hasThumbnail, + sizeInBytes: sizeInBytes ?? this.sizeInBytes, + createdAt: createdAt ?? this.createdAt, + createdAtMonth: createdAtMonth ?? this.createdAtMonth, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (uploadState.present) { + map['upload_state'] = Variable(uploadState.value); + } + if (cloudState.present) { + map['cloud_state'] = Variable(cloudState.value); + } + if (blurhash.present) { + map['blurhash'] = Variable(blurhash.value); + } + if (downloadState.present) { + map['download_state'] = Variable(downloadState.value); + } + if (requiresAuthentication.present) { + map['requires_authentication'] = Variable( + requiresAuthentication.value, + ); + } + if (stored.present) { + map['stored'] = Variable(stored.value); + } + if (isDraftMedia.present) { + map['is_draft_media'] = Variable(isDraftMedia.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (hasCropAnalyzed.present) { + map['has_crop_analyzed'] = Variable(hasCropAnalyzed.value); + } + if (preProgressingProcess.present) { + map['pre_progressing_process'] = Variable( + preProgressingProcess.value, + ); + } + if (reuploadRequestedBy.present) { + map['reupload_requested_by'] = Variable( + reuploadRequestedBy.value, + ); + } + if (displayLimitInMilliseconds.present) { + map['display_limit_in_milliseconds'] = Variable( + displayLimitInMilliseconds.value, + ); + } + if (removeAudio.present) { + map['remove_audio'] = Variable(removeAudio.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (encryptionKey.present) { + map['encryption_key'] = Variable(encryptionKey.value); + } + if (encryptionMac.present) { + map['encryption_mac'] = Variable(encryptionMac.value); + } + if (encryptionNonce.present) { + map['encryption_nonce'] = Variable(encryptionNonce.value); + } + if (storedFileHash.present) { + map['stored_file_hash'] = Variable(storedFileHash.value); + } + if (hasThumbnail.present) { + map['has_thumbnail'] = Variable(hasThumbnail.value); + } + if (sizeInBytes.present) { + map['size_in_bytes'] = Variable(sizeInBytes.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (createdAtMonth.present) { + map['created_at_month'] = Variable(createdAtMonth.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MediaFilesCompanion(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('cloudState: $cloudState, ') + ..write('blurhash: $blurhash, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('isFavorite: $isFavorite, ') + ..write('hasCropAnalyzed: $hasCropAnalyzed, ') + ..write('preProgressingProcess: $preProgressingProcess, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('storedFileHash: $storedFileHash, ') + ..write('hasThumbnail: $hasThumbnail, ') + ..write('sizeInBytes: $sizeInBytes, ') + ..write('createdAt: $createdAt, ') + ..write('createdAtMonth: $createdAtMonth, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Messages extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Messages(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: + 'NULL REFERENCES media_files(media_id)ON DELETE SET NULL', + ); + late final GeneratedColumn additionalMessageData = + GeneratedColumn( + 'additional_message_data', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn mediaStored = GeneratedColumn( + 'media_stored', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (media_stored IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn mediaReopened = GeneratedColumn( + 'media_reopened', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (media_reopened IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn downloadToken = + GeneratedColumn( + 'download_token', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn quotesMessageId = GeneratedColumn( + 'quotes_message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn isDeletedFromSender = GeneratedColumn( + 'is_deleted_from_sender', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (is_deleted_from_sender IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn openedAt = GeneratedColumn( + 'opened_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn openedByAll = GeneratedColumn( + 'opened_by_all', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + late final GeneratedColumn modifiedAt = GeneratedColumn( + 'modified_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ackByUser = GeneratedColumn( + 'ack_by_user', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ackByServer = GeneratedColumn( + 'ack_by_server', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + groupId, + messageId, + senderId, + type, + content, + mediaId, + additionalMessageData, + mediaStored, + mediaReopened, + downloadToken, + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'messages'; + @override + Set get $primaryKey => {messageId}; + @override + MessagesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessagesData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + senderId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sender_id'], + ), + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + content: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + ), + mediaId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}media_id'], + ), + additionalMessageData: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}additional_message_data'], + ), + mediaStored: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_stored'], + )!, + mediaReopened: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}media_reopened'], + )!, + downloadToken: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}download_token'], + ), + quotesMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}quotes_message_id'], + ), + isDeletedFromSender: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_deleted_from_sender'], + )!, + openedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}opened_at'], + ), + openedByAll: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}opened_by_all'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + modifiedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}modified_at'], + ), + ackByUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ack_by_user'], + ), + ackByServer: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ack_by_server'], + ), + ); + } + + @override + Messages createAlias(String alias) { + return Messages(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(message_id)']; + @override + bool get dontWriteConstraints => true; +} + +class MessagesData extends DataClass implements Insertable { + final String groupId; + final String messageId; + final int? senderId; + final String type; + final String? content; + final String? mediaId; + final i2.Uint8List? additionalMessageData; + final int mediaStored; + final int mediaReopened; + final i2.Uint8List? downloadToken; + final String? quotesMessageId; + final int isDeletedFromSender; + final int? openedAt; + final int? openedByAll; + final int createdAt; + final int? modifiedAt; + final int? ackByUser; + final int? ackByServer; + const MessagesData({ + required this.groupId, + required this.messageId, + this.senderId, + required this.type, + this.content, + this.mediaId, + this.additionalMessageData, + required this.mediaStored, + required this.mediaReopened, + this.downloadToken, + this.quotesMessageId, + required this.isDeletedFromSender, + this.openedAt, + this.openedByAll, + required this.createdAt, + this.modifiedAt, + this.ackByUser, + this.ackByServer, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + map['type'] = Variable(type); + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + if (!nullToAbsent || mediaId != null) { + map['media_id'] = Variable(mediaId); + } + if (!nullToAbsent || additionalMessageData != null) { + map['additional_message_data'] = Variable( + additionalMessageData, + ); + } + map['media_stored'] = Variable(mediaStored); + map['media_reopened'] = Variable(mediaReopened); + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || quotesMessageId != null) { + map['quotes_message_id'] = Variable(quotesMessageId); + } + map['is_deleted_from_sender'] = Variable(isDeletedFromSender); + if (!nullToAbsent || openedAt != null) { + map['opened_at'] = Variable(openedAt); + } + if (!nullToAbsent || openedByAll != null) { + map['opened_by_all'] = Variable(openedByAll); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || modifiedAt != null) { + map['modified_at'] = Variable(modifiedAt); + } + if (!nullToAbsent || ackByUser != null) { + map['ack_by_user'] = Variable(ackByUser); + } + if (!nullToAbsent || ackByServer != null) { + map['ack_by_server'] = Variable(ackByServer); + } + return map; + } + + MessagesCompanion toCompanion(bool nullToAbsent) { + return MessagesCompanion( + groupId: Value(groupId), + messageId: Value(messageId), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + type: Value(type), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + mediaId: mediaId == null && nullToAbsent + ? const Value.absent() + : Value(mediaId), + additionalMessageData: additionalMessageData == null && nullToAbsent + ? const Value.absent() + : Value(additionalMessageData), + mediaStored: Value(mediaStored), + mediaReopened: Value(mediaReopened), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + quotesMessageId: quotesMessageId == null && nullToAbsent + ? const Value.absent() + : Value(quotesMessageId), + isDeletedFromSender: Value(isDeletedFromSender), + openedAt: openedAt == null && nullToAbsent + ? const Value.absent() + : Value(openedAt), + openedByAll: openedByAll == null && nullToAbsent + ? const Value.absent() + : Value(openedByAll), + createdAt: Value(createdAt), + modifiedAt: modifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(modifiedAt), + ackByUser: ackByUser == null && nullToAbsent + ? const Value.absent() + : Value(ackByUser), + ackByServer: ackByServer == null && nullToAbsent + ? const Value.absent() + : Value(ackByServer), + ); + } + + factory MessagesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessagesData( + groupId: serializer.fromJson(json['groupId']), + messageId: serializer.fromJson(json['messageId']), + senderId: serializer.fromJson(json['senderId']), + type: serializer.fromJson(json['type']), + content: serializer.fromJson(json['content']), + mediaId: serializer.fromJson(json['mediaId']), + additionalMessageData: serializer.fromJson( + json['additionalMessageData'], + ), + mediaStored: serializer.fromJson(json['mediaStored']), + mediaReopened: serializer.fromJson(json['mediaReopened']), + downloadToken: serializer.fromJson(json['downloadToken']), + quotesMessageId: serializer.fromJson(json['quotesMessageId']), + isDeletedFromSender: serializer.fromJson( + json['isDeletedFromSender'], + ), + openedAt: serializer.fromJson(json['openedAt']), + openedByAll: serializer.fromJson(json['openedByAll']), + createdAt: serializer.fromJson(json['createdAt']), + modifiedAt: serializer.fromJson(json['modifiedAt']), + ackByUser: serializer.fromJson(json['ackByUser']), + ackByServer: serializer.fromJson(json['ackByServer']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'messageId': serializer.toJson(messageId), + 'senderId': serializer.toJson(senderId), + 'type': serializer.toJson(type), + 'content': serializer.toJson(content), + 'mediaId': serializer.toJson(mediaId), + 'additionalMessageData': serializer.toJson( + additionalMessageData, + ), + 'mediaStored': serializer.toJson(mediaStored), + 'mediaReopened': serializer.toJson(mediaReopened), + 'downloadToken': serializer.toJson(downloadToken), + 'quotesMessageId': serializer.toJson(quotesMessageId), + 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), + 'openedAt': serializer.toJson(openedAt), + 'openedByAll': serializer.toJson(openedByAll), + 'createdAt': serializer.toJson(createdAt), + 'modifiedAt': serializer.toJson(modifiedAt), + 'ackByUser': serializer.toJson(ackByUser), + 'ackByServer': serializer.toJson(ackByServer), + }; + } + + MessagesData copyWith({ + String? groupId, + String? messageId, + Value senderId = const Value.absent(), + String? type, + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value additionalMessageData = const Value.absent(), + int? mediaStored, + int? mediaReopened, + Value downloadToken = const Value.absent(), + Value quotesMessageId = const Value.absent(), + int? isDeletedFromSender, + Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), + int? createdAt, + Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), + }) => MessagesData( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId.present ? senderId.value : this.senderId, + type: type ?? this.type, + content: content.present ? content.value : this.content, + mediaId: mediaId.present ? mediaId.value : this.mediaId, + additionalMessageData: additionalMessageData.present + ? additionalMessageData.value + : this.additionalMessageData, + mediaStored: mediaStored ?? this.mediaStored, + mediaReopened: mediaReopened ?? this.mediaReopened, + downloadToken: downloadToken.present + ? downloadToken.value + : this.downloadToken, + quotesMessageId: quotesMessageId.present + ? quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt.present ? openedAt.value : this.openedAt, + openedByAll: openedByAll.present ? openedByAll.value : this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, + ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser, + ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer, + ); + MessagesData copyWithCompanion(MessagesCompanion data) { + return MessagesData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + type: data.type.present ? data.type.value : this.type, + content: data.content.present ? data.content.value : this.content, + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + additionalMessageData: data.additionalMessageData.present + ? data.additionalMessageData.value + : this.additionalMessageData, + mediaStored: data.mediaStored.present + ? data.mediaStored.value + : this.mediaStored, + mediaReopened: data.mediaReopened.present + ? data.mediaReopened.value + : this.mediaReopened, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + quotesMessageId: data.quotesMessageId.present + ? data.quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: data.isDeletedFromSender.present + ? data.isDeletedFromSender.value + : this.isDeletedFromSender, + openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, + openedByAll: data.openedByAll.present + ? data.openedByAll.value + : this.openedByAll, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + modifiedAt: data.modifiedAt.present + ? data.modifiedAt.value + : this.modifiedAt, + ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, + ackByServer: data.ackByServer.present + ? data.ackByServer.value + : this.ackByServer, + ); + } + + @override + String toString() { + return (StringBuffer('MessagesData(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') + ..write('mediaStored: $mediaStored, ') + ..write('mediaReopened: $mediaReopened, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + messageId, + senderId, + type, + content, + mediaId, + $driftBlobEquality.hash(additionalMessageData), + mediaStored, + mediaReopened, + $driftBlobEquality.hash(downloadToken), + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessagesData && + other.groupId == this.groupId && + other.messageId == this.messageId && + other.senderId == this.senderId && + other.type == this.type && + other.content == this.content && + other.mediaId == this.mediaId && + $driftBlobEquality.equals( + other.additionalMessageData, + this.additionalMessageData, + ) && + other.mediaStored == this.mediaStored && + other.mediaReopened == this.mediaReopened && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + other.quotesMessageId == this.quotesMessageId && + other.isDeletedFromSender == this.isDeletedFromSender && + other.openedAt == this.openedAt && + other.openedByAll == this.openedByAll && + other.createdAt == this.createdAt && + other.modifiedAt == this.modifiedAt && + other.ackByUser == this.ackByUser && + other.ackByServer == this.ackByServer); +} + +class MessagesCompanion extends UpdateCompanion { + final Value groupId; + final Value messageId; + final Value senderId; + final Value type; + final Value content; + final Value mediaId; + final Value additionalMessageData; + final Value mediaStored; + final Value mediaReopened; + final Value downloadToken; + final Value quotesMessageId; + final Value isDeletedFromSender; + final Value openedAt; + final Value openedByAll; + final Value createdAt; + final Value modifiedAt; + final Value ackByUser; + final Value ackByServer; + final Value rowid; + const MessagesCompanion({ + this.groupId = const Value.absent(), + this.messageId = const Value.absent(), + this.senderId = const Value.absent(), + this.type = const Value.absent(), + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), + this.mediaStored = const Value.absent(), + this.mediaReopened = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessagesCompanion.insert({ + required String groupId, + required String messageId, + this.senderId = const Value.absent(), + required String type, + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.additionalMessageData = const Value.absent(), + this.mediaStored = const Value.absent(), + this.mediaReopened = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + messageId = Value(messageId), + type = Value(type); + static Insertable custom({ + Expression? groupId, + Expression? messageId, + Expression? senderId, + Expression? type, + Expression? content, + Expression? mediaId, + Expression? additionalMessageData, + Expression? mediaStored, + Expression? mediaReopened, + Expression? downloadToken, + Expression? quotesMessageId, + Expression? isDeletedFromSender, + Expression? openedAt, + Expression? openedByAll, + Expression? createdAt, + Expression? modifiedAt, + Expression? ackByUser, + Expression? ackByServer, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (messageId != null) 'message_id': messageId, + if (senderId != null) 'sender_id': senderId, + if (type != null) 'type': type, + if (content != null) 'content': content, + if (mediaId != null) 'media_id': mediaId, + if (additionalMessageData != null) + 'additional_message_data': additionalMessageData, + if (mediaStored != null) 'media_stored': mediaStored, + if (mediaReopened != null) 'media_reopened': mediaReopened, + if (downloadToken != null) 'download_token': downloadToken, + if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, + if (isDeletedFromSender != null) + 'is_deleted_from_sender': isDeletedFromSender, + if (openedAt != null) 'opened_at': openedAt, + if (openedByAll != null) 'opened_by_all': openedByAll, + if (createdAt != null) 'created_at': createdAt, + if (modifiedAt != null) 'modified_at': modifiedAt, + if (ackByUser != null) 'ack_by_user': ackByUser, + if (ackByServer != null) 'ack_by_server': ackByServer, + if (rowid != null) 'rowid': rowid, + }); + } + + MessagesCompanion copyWith({ + Value? groupId, + Value? messageId, + Value? senderId, + Value? type, + Value? content, + Value? mediaId, + Value? additionalMessageData, + Value? mediaStored, + Value? mediaReopened, + Value? downloadToken, + Value? quotesMessageId, + Value? isDeletedFromSender, + Value? openedAt, + Value? openedByAll, + Value? createdAt, + Value? modifiedAt, + Value? ackByUser, + Value? ackByServer, + Value? rowid, + }) { + return MessagesCompanion( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId ?? this.senderId, + type: type ?? this.type, + content: content ?? this.content, + mediaId: mediaId ?? this.mediaId, + additionalMessageData: + additionalMessageData ?? this.additionalMessageData, + mediaStored: mediaStored ?? this.mediaStored, + mediaReopened: mediaReopened ?? this.mediaReopened, + downloadToken: downloadToken ?? this.downloadToken, + quotesMessageId: quotesMessageId ?? this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt ?? this.openedAt, + openedByAll: openedByAll ?? this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt ?? this.modifiedAt, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (additionalMessageData.present) { + map['additional_message_data'] = Variable( + additionalMessageData.value, + ); + } + if (mediaStored.present) { + map['media_stored'] = Variable(mediaStored.value); + } + if (mediaReopened.present) { + map['media_reopened'] = Variable(mediaReopened.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (quotesMessageId.present) { + map['quotes_message_id'] = Variable(quotesMessageId.value); + } + if (isDeletedFromSender.present) { + map['is_deleted_from_sender'] = Variable(isDeletedFromSender.value); + } + if (openedAt.present) { + map['opened_at'] = Variable(openedAt.value); + } + if (openedByAll.present) { + map['opened_by_all'] = Variable(openedByAll.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (modifiedAt.present) { + map['modified_at'] = Variable(modifiedAt.value); + } + if (ackByUser.present) { + map['ack_by_user'] = Variable(ackByUser.value); + } + if (ackByServer.present) { + map['ack_by_server'] = Variable(ackByServer.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessagesCompanion(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('additionalMessageData: $additionalMessageData, ') + ..write('mediaStored: $mediaStored, ') + ..write('mediaReopened: $mediaReopened, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class MessageHistories extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MessageHistories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn content = GeneratedColumn( + 'content', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + id, + messageId, + contactId, + content, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_histories'; + @override + Set get $primaryKey => {id}; + @override + MessageHistoriesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageHistoriesData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + ), + content: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}content'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + MessageHistories createAlias(String alias) { + return MessageHistories(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class MessageHistoriesData extends DataClass + implements Insertable { + final int id; + final String messageId; + final int? contactId; + final String? content; + final int createdAt; + const MessageHistoriesData({ + required this.id, + required this.messageId, + this.contactId, + this.content, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MessageHistoriesCompanion toCompanion(bool nullToAbsent) { + return MessageHistoriesCompanion( + id: Value(id), + messageId: Value(messageId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + createdAt: Value(createdAt), + ); + } + + factory MessageHistoriesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageHistoriesData( + id: serializer.fromJson(json['id']), + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + content: serializer.fromJson(json['content']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'content': serializer.toJson(content), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MessageHistoriesData copyWith({ + int? id, + String? messageId, + Value contactId = const Value.absent(), + Value content = const Value.absent(), + int? createdAt, + }) => MessageHistoriesData( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId.present ? contactId.value : this.contactId, + content: content.present ? content.value : this.content, + createdAt: createdAt ?? this.createdAt, + ); + MessageHistoriesData copyWithCompanion(MessageHistoriesCompanion data) { + return MessageHistoriesData( + id: data.id.present ? data.id.value : this.id, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + content: data.content.present ? data.content.value : this.content, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesData(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, messageId, contactId, content, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageHistoriesData && + other.id == this.id && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.content == this.content && + other.createdAt == this.createdAt); +} + +class MessageHistoriesCompanion extends UpdateCompanion { + final Value id; + final Value messageId; + final Value contactId; + final Value content; + final Value createdAt; + const MessageHistoriesCompanion({ + this.id = const Value.absent(), + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }); + MessageHistoriesCompanion.insert({ + this.id = const Value.absent(), + required String messageId, + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }) : messageId = Value(messageId); + static Insertable custom({ + Expression? id, + Expression? messageId, + Expression? contactId, + Expression? content, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (content != null) 'content': content, + if (createdAt != null) 'created_at': createdAt, + }); + } + + MessageHistoriesCompanion copyWith({ + Value? id, + Value? messageId, + Value? contactId, + Value? content, + Value? createdAt, + }) { + return MessageHistoriesCompanion( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + content: content ?? this.content, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesCompanion(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class Reactions extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Reactions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [messageId, emoji, senderId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'reactions'; + @override + Set get $primaryKey => {messageId, senderId, emoji}; + @override + ReactionsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReactionsData( + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + senderId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sender_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + Reactions createAlias(String alias) { + return Reactions(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(message_id, sender_id, emoji)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class ReactionsData extends DataClass implements Insertable { + final String messageId; + final String emoji; + final int? senderId; + final int createdAt; + const ReactionsData({ + required this.messageId, + required this.emoji, + this.senderId, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['emoji'] = Variable(emoji); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReactionsCompanion toCompanion(bool nullToAbsent) { + return ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + createdAt: Value(createdAt), + ); + } + + factory ReactionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReactionsData( + messageId: serializer.fromJson(json['messageId']), + emoji: serializer.fromJson(json['emoji']), + senderId: serializer.fromJson(json['senderId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'emoji': serializer.toJson(emoji), + 'senderId': serializer.toJson(senderId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReactionsData copyWith({ + String? messageId, + String? emoji, + Value senderId = const Value.absent(), + int? createdAt, + }) => ReactionsData( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId.present ? senderId.value : this.senderId, + createdAt: createdAt ?? this.createdAt, + ); + ReactionsData copyWithCompanion(ReactionsCompanion data) { + return ReactionsData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReactionsData(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, emoji, senderId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReactionsData && + other.messageId == this.messageId && + other.emoji == this.emoji && + other.senderId == this.senderId && + other.createdAt == this.createdAt); +} + +class ReactionsCompanion extends UpdateCompanion { + final Value messageId; + final Value emoji; + final Value senderId; + final Value createdAt; + final Value rowid; + const ReactionsCompanion({ + this.messageId = const Value.absent(), + this.emoji = const Value.absent(), + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReactionsCompanion.insert({ + required String messageId, + required String emoji, + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + emoji = Value(emoji); + static Insertable custom({ + Expression? messageId, + Expression? emoji, + Expression? senderId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (emoji != null) 'emoji': emoji, + if (senderId != null) 'sender_id': senderId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReactionsCompanion copyWith({ + Value? messageId, + Value? emoji, + Value? senderId, + Value? createdAt, + Value? rowid, + }) { + return ReactionsCompanion( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId ?? this.senderId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReactionsCompanion(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class GroupMembers extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GroupMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES contacts(user_id)', + ); + late final GeneratedColumn memberState = GeneratedColumn( + 'member_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn groupPublicKey = + GeneratedColumn( + 'group_public_key', + aliasedName, + true, + type: DriftSqlType.blob, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastChatOpened = GeneratedColumn( + 'last_chat_opened', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastTypeIndicator = GeneratedColumn( + 'last_type_indicator', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn lastMessage = GeneratedColumn( + 'last_message', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + groupId, + contactId, + memberState, + groupPublicKey, + lastChatOpened, + lastTypeIndicator, + lastMessage, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_members'; + @override + Set get $primaryKey => {groupId, contactId}; + @override + GroupMembersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupMembersData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + memberState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}member_state'], + ), + groupPublicKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}group_public_key'], + ), + lastChatOpened: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_chat_opened'], + ), + lastTypeIndicator: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_type_indicator'], + ), + lastMessage: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_message'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + GroupMembers createAlias(String alias) { + return GroupMembers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(group_id, contact_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class GroupMembersData extends DataClass + implements Insertable { + final String groupId; + final int contactId; + final String? memberState; + final i2.Uint8List? groupPublicKey; + final int? lastChatOpened; + final int? lastTypeIndicator; + final int? lastMessage; + final int createdAt; + const GroupMembersData({ + required this.groupId, + required this.contactId, + this.memberState, + this.groupPublicKey, + this.lastChatOpened, + this.lastTypeIndicator, + this.lastMessage, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || memberState != null) { + map['member_state'] = Variable(memberState); + } + if (!nullToAbsent || groupPublicKey != null) { + map['group_public_key'] = Variable(groupPublicKey); + } + if (!nullToAbsent || lastChatOpened != null) { + map['last_chat_opened'] = Variable(lastChatOpened); + } + if (!nullToAbsent || lastTypeIndicator != null) { + map['last_type_indicator'] = Variable(lastTypeIndicator); + } + if (!nullToAbsent || lastMessage != null) { + map['last_message'] = Variable(lastMessage); + } + map['created_at'] = Variable(createdAt); + return map; + } + + GroupMembersCompanion toCompanion(bool nullToAbsent) { + return GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(contactId), + memberState: memberState == null && nullToAbsent + ? const Value.absent() + : Value(memberState), + groupPublicKey: groupPublicKey == null && nullToAbsent + ? const Value.absent() + : Value(groupPublicKey), + lastChatOpened: lastChatOpened == null && nullToAbsent + ? const Value.absent() + : Value(lastChatOpened), + lastTypeIndicator: lastTypeIndicator == null && nullToAbsent + ? const Value.absent() + : Value(lastTypeIndicator), + lastMessage: lastMessage == null && nullToAbsent + ? const Value.absent() + : Value(lastMessage), + createdAt: Value(createdAt), + ); + } + + factory GroupMembersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupMembersData( + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + memberState: serializer.fromJson(json['memberState']), + groupPublicKey: serializer.fromJson( + json['groupPublicKey'], + ), + lastChatOpened: serializer.fromJson(json['lastChatOpened']), + lastTypeIndicator: serializer.fromJson(json['lastTypeIndicator']), + lastMessage: serializer.fromJson(json['lastMessage']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'memberState': serializer.toJson(memberState), + 'groupPublicKey': serializer.toJson(groupPublicKey), + 'lastChatOpened': serializer.toJson(lastChatOpened), + 'lastTypeIndicator': serializer.toJson(lastTypeIndicator), + 'lastMessage': serializer.toJson(lastMessage), + 'createdAt': serializer.toJson(createdAt), + }; + } + + GroupMembersData copyWith({ + String? groupId, + int? contactId, + Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), + Value lastChatOpened = const Value.absent(), + Value lastTypeIndicator = const Value.absent(), + Value lastMessage = const Value.absent(), + int? createdAt, + }) => GroupMembersData( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState.present ? memberState.value : this.memberState, + groupPublicKey: groupPublicKey.present + ? groupPublicKey.value + : this.groupPublicKey, + lastChatOpened: lastChatOpened.present + ? lastChatOpened.value + : this.lastChatOpened, + lastTypeIndicator: lastTypeIndicator.present + ? lastTypeIndicator.value + : this.lastTypeIndicator, + lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, + createdAt: createdAt ?? this.createdAt, + ); + GroupMembersData copyWithCompanion(GroupMembersCompanion data) { + return GroupMembersData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + memberState: data.memberState.present + ? data.memberState.value + : this.memberState, + groupPublicKey: data.groupPublicKey.present + ? data.groupPublicKey.value + : this.groupPublicKey, + lastChatOpened: data.lastChatOpened.present + ? data.lastChatOpened.value + : this.lastChatOpened, + lastTypeIndicator: data.lastTypeIndicator.present + ? data.lastTypeIndicator.value + : this.lastTypeIndicator, + lastMessage: data.lastMessage.present + ? data.lastMessage.value + : this.lastMessage, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupMembersData(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastChatOpened: $lastChatOpened, ') + ..write('lastTypeIndicator: $lastTypeIndicator, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + contactId, + memberState, + $driftBlobEquality.hash(groupPublicKey), + lastChatOpened, + lastTypeIndicator, + lastMessage, + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupMembersData && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.memberState == this.memberState && + $driftBlobEquality.equals( + other.groupPublicKey, + this.groupPublicKey, + ) && + other.lastChatOpened == this.lastChatOpened && + other.lastTypeIndicator == this.lastTypeIndicator && + other.lastMessage == this.lastMessage && + other.createdAt == this.createdAt); +} + +class GroupMembersCompanion extends UpdateCompanion { + final Value groupId; + final Value contactId; + final Value memberState; + final Value groupPublicKey; + final Value lastChatOpened; + final Value lastTypeIndicator; + final Value lastMessage; + final Value createdAt; + final Value rowid; + const GroupMembersCompanion({ + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastChatOpened = const Value.absent(), + this.lastTypeIndicator = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupMembersCompanion.insert({ + required String groupId, + required int contactId, + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastChatOpened = const Value.absent(), + this.lastTypeIndicator = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + contactId = Value(contactId); + static Insertable custom({ + Expression? groupId, + Expression? contactId, + Expression? memberState, + Expression? groupPublicKey, + Expression? lastChatOpened, + Expression? lastTypeIndicator, + Expression? lastMessage, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (memberState != null) 'member_state': memberState, + if (groupPublicKey != null) 'group_public_key': groupPublicKey, + if (lastChatOpened != null) 'last_chat_opened': lastChatOpened, + if (lastTypeIndicator != null) 'last_type_indicator': lastTypeIndicator, + if (lastMessage != null) 'last_message': lastMessage, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupMembersCompanion copyWith({ + Value? groupId, + Value? contactId, + Value? memberState, + Value? groupPublicKey, + Value? lastChatOpened, + Value? lastTypeIndicator, + Value? lastMessage, + Value? createdAt, + Value? rowid, + }) { + return GroupMembersCompanion( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState ?? this.memberState, + groupPublicKey: groupPublicKey ?? this.groupPublicKey, + lastChatOpened: lastChatOpened ?? this.lastChatOpened, + lastTypeIndicator: lastTypeIndicator ?? this.lastTypeIndicator, + lastMessage: lastMessage ?? this.lastMessage, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (memberState.present) { + map['member_state'] = Variable(memberState.value); + } + if (groupPublicKey.present) { + map['group_public_key'] = Variable(groupPublicKey.value); + } + if (lastChatOpened.present) { + map['last_chat_opened'] = Variable(lastChatOpened.value); + } + if (lastTypeIndicator.present) { + map['last_type_indicator'] = Variable(lastTypeIndicator.value); + } + if (lastMessage.present) { + map['last_message'] = Variable(lastMessage.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupMembersCompanion(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastChatOpened: $lastChatOpened, ') + ..write('lastTypeIndicator: $lastTypeIndicator, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Receipts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Receipts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn message = + GeneratedColumn( + 'message', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn contactWillSendsReceipt = + GeneratedColumn( + 'contact_will_sends_receipt', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 1 CHECK (contact_will_sends_receipt IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn + willBeRetriedByMediaUpload = GeneratedColumn( + 'will_be_retried_by_media_upload', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn markForRetry = GeneratedColumn( + 'mark_for_retry', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn markForRetryAfterAccepted = + GeneratedColumn( + 'mark_for_retry_after_accepted', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn ackByServerAt = GeneratedColumn( + 'ack_by_server_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn retryCount = GeneratedColumn( + 'retry_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn lastRetry = GeneratedColumn( + 'last_retry', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + receiptId, + contactId, + messageId, + message, + contactWillSendsReceipt, + willBeRetriedByMediaUpload, + markForRetry, + markForRetryAfterAccepted, + ackByServerAt, + retryCount, + lastRetry, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'receipts'; + @override + Set get $primaryKey => {receiptId}; + @override + ReceiptsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceiptsData( + receiptId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}receipt_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + ), + message: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}message'], + )!, + contactWillSendsReceipt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_will_sends_receipt'], + )!, + willBeRetriedByMediaUpload: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}will_be_retried_by_media_upload'], + )!, + markForRetry: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}mark_for_retry'], + ), + markForRetryAfterAccepted: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}mark_for_retry_after_accepted'], + ), + ackByServerAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}ack_by_server_at'], + ), + retryCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}retry_count'], + )!, + lastRetry: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_retry'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + Receipts createAlias(String alias) { + return Receipts(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(receipt_id)']; + @override + bool get dontWriteConstraints => true; +} + +class ReceiptsData extends DataClass implements Insertable { + final String receiptId; + final int contactId; + final String? messageId; + final i2.Uint8List message; + final int contactWillSendsReceipt; + final int willBeRetriedByMediaUpload; + final int? markForRetry; + final int? markForRetryAfterAccepted; + final int? ackByServerAt; + final int retryCount; + final int? lastRetry; + final int createdAt; + const ReceiptsData({ + required this.receiptId, + required this.contactId, + this.messageId, + required this.message, + required this.contactWillSendsReceipt, + required this.willBeRetriedByMediaUpload, + this.markForRetry, + this.markForRetryAfterAccepted, + this.ackByServerAt, + required this.retryCount, + this.lastRetry, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + map['message'] = Variable(message); + map['contact_will_sends_receipt'] = Variable(contactWillSendsReceipt); + map['will_be_retried_by_media_upload'] = Variable( + willBeRetriedByMediaUpload, + ); + if (!nullToAbsent || markForRetry != null) { + map['mark_for_retry'] = Variable(markForRetry); + } + if (!nullToAbsent || markForRetryAfterAccepted != null) { + map['mark_for_retry_after_accepted'] = Variable( + markForRetryAfterAccepted, + ); + } + if (!nullToAbsent || ackByServerAt != null) { + map['ack_by_server_at'] = Variable(ackByServerAt); + } + map['retry_count'] = Variable(retryCount); + if (!nullToAbsent || lastRetry != null) { + map['last_retry'] = Variable(lastRetry); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(contactId), + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + message: Value(message), + contactWillSendsReceipt: Value(contactWillSendsReceipt), + willBeRetriedByMediaUpload: Value(willBeRetriedByMediaUpload), + markForRetry: markForRetry == null && nullToAbsent + ? const Value.absent() + : Value(markForRetry), + markForRetryAfterAccepted: + markForRetryAfterAccepted == null && nullToAbsent + ? const Value.absent() + : Value(markForRetryAfterAccepted), + ackByServerAt: ackByServerAt == null && nullToAbsent + ? const Value.absent() + : Value(ackByServerAt), + retryCount: Value(retryCount), + lastRetry: lastRetry == null && nullToAbsent + ? const Value.absent() + : Value(lastRetry), + createdAt: Value(createdAt), + ); + } + + factory ReceiptsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceiptsData( + receiptId: serializer.fromJson(json['receiptId']), + contactId: serializer.fromJson(json['contactId']), + messageId: serializer.fromJson(json['messageId']), + message: serializer.fromJson(json['message']), + contactWillSendsReceipt: serializer.fromJson( + json['contactWillSendsReceipt'], + ), + willBeRetriedByMediaUpload: serializer.fromJson( + json['willBeRetriedByMediaUpload'], + ), + markForRetry: serializer.fromJson(json['markForRetry']), + markForRetryAfterAccepted: serializer.fromJson( + json['markForRetryAfterAccepted'], + ), + ackByServerAt: serializer.fromJson(json['ackByServerAt']), + retryCount: serializer.fromJson(json['retryCount']), + lastRetry: serializer.fromJson(json['lastRetry']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'contactId': serializer.toJson(contactId), + 'messageId': serializer.toJson(messageId), + 'message': serializer.toJson(message), + 'contactWillSendsReceipt': serializer.toJson( + contactWillSendsReceipt, + ), + 'willBeRetriedByMediaUpload': serializer.toJson( + willBeRetriedByMediaUpload, + ), + 'markForRetry': serializer.toJson(markForRetry), + 'markForRetryAfterAccepted': serializer.toJson( + markForRetryAfterAccepted, + ), + 'ackByServerAt': serializer.toJson(ackByServerAt), + 'retryCount': serializer.toJson(retryCount), + 'lastRetry': serializer.toJson(lastRetry), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReceiptsData copyWith({ + String? receiptId, + int? contactId, + Value messageId = const Value.absent(), + i2.Uint8List? message, + int? contactWillSendsReceipt, + int? willBeRetriedByMediaUpload, + Value markForRetry = const Value.absent(), + Value markForRetryAfterAccepted = const Value.absent(), + Value ackByServerAt = const Value.absent(), + int? retryCount, + Value lastRetry = const Value.absent(), + int? createdAt, + }) => ReceiptsData( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId.present ? messageId.value : this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + willBeRetriedByMediaUpload: + willBeRetriedByMediaUpload ?? this.willBeRetriedByMediaUpload, + markForRetry: markForRetry.present ? markForRetry.value : this.markForRetry, + markForRetryAfterAccepted: markForRetryAfterAccepted.present + ? markForRetryAfterAccepted.value + : this.markForRetryAfterAccepted, + ackByServerAt: ackByServerAt.present + ? ackByServerAt.value + : this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry.present ? lastRetry.value : this.lastRetry, + createdAt: createdAt ?? this.createdAt, + ); + ReceiptsData copyWithCompanion(ReceiptsCompanion data) { + return ReceiptsData( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + message: data.message.present ? data.message.value : this.message, + contactWillSendsReceipt: data.contactWillSendsReceipt.present + ? data.contactWillSendsReceipt.value + : this.contactWillSendsReceipt, + willBeRetriedByMediaUpload: data.willBeRetriedByMediaUpload.present + ? data.willBeRetriedByMediaUpload.value + : this.willBeRetriedByMediaUpload, + markForRetry: data.markForRetry.present + ? data.markForRetry.value + : this.markForRetry, + markForRetryAfterAccepted: data.markForRetryAfterAccepted.present + ? data.markForRetryAfterAccepted.value + : this.markForRetryAfterAccepted, + ackByServerAt: data.ackByServerAt.present + ? data.ackByServerAt.value + : this.ackByServerAt, + retryCount: data.retryCount.present + ? data.retryCount.value + : this.retryCount, + lastRetry: data.lastRetry.present ? data.lastRetry.value : this.lastRetry, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceiptsData(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('willBeRetriedByMediaUpload: $willBeRetriedByMediaUpload, ') + ..write('markForRetry: $markForRetry, ') + ..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + receiptId, + contactId, + messageId, + $driftBlobEquality.hash(message), + contactWillSendsReceipt, + willBeRetriedByMediaUpload, + markForRetry, + markForRetryAfterAccepted, + ackByServerAt, + retryCount, + lastRetry, + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceiptsData && + other.receiptId == this.receiptId && + other.contactId == this.contactId && + other.messageId == this.messageId && + $driftBlobEquality.equals(other.message, this.message) && + other.contactWillSendsReceipt == this.contactWillSendsReceipt && + other.willBeRetriedByMediaUpload == this.willBeRetriedByMediaUpload && + other.markForRetry == this.markForRetry && + other.markForRetryAfterAccepted == this.markForRetryAfterAccepted && + other.ackByServerAt == this.ackByServerAt && + other.retryCount == this.retryCount && + other.lastRetry == this.lastRetry && + other.createdAt == this.createdAt); +} + +class ReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value contactId; + final Value messageId; + final Value message; + final Value contactWillSendsReceipt; + final Value willBeRetriedByMediaUpload; + final Value markForRetry; + final Value markForRetryAfterAccepted; + final Value ackByServerAt; + final Value retryCount; + final Value lastRetry; + final Value createdAt; + final Value rowid; + const ReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.contactId = const Value.absent(), + this.messageId = const Value.absent(), + this.message = const Value.absent(), + this.contactWillSendsReceipt = const Value.absent(), + this.willBeRetriedByMediaUpload = const Value.absent(), + this.markForRetry = const Value.absent(), + this.markForRetryAfterAccepted = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceiptsCompanion.insert({ + required String receiptId, + required int contactId, + this.messageId = const Value.absent(), + required i2.Uint8List message, + this.contactWillSendsReceipt = const Value.absent(), + this.willBeRetriedByMediaUpload = const Value.absent(), + this.markForRetry = const Value.absent(), + this.markForRetryAfterAccepted = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId), + contactId = Value(contactId), + message = Value(message); + static Insertable custom({ + Expression? receiptId, + Expression? contactId, + Expression? messageId, + Expression? message, + Expression? contactWillSendsReceipt, + Expression? willBeRetriedByMediaUpload, + Expression? markForRetry, + Expression? markForRetryAfterAccepted, + Expression? ackByServerAt, + Expression? retryCount, + Expression? lastRetry, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (contactId != null) 'contact_id': contactId, + if (messageId != null) 'message_id': messageId, + if (message != null) 'message': message, + if (contactWillSendsReceipt != null) + 'contact_will_sends_receipt': contactWillSendsReceipt, + if (willBeRetriedByMediaUpload != null) + 'will_be_retried_by_media_upload': willBeRetriedByMediaUpload, + if (markForRetry != null) 'mark_for_retry': markForRetry, + if (markForRetryAfterAccepted != null) + 'mark_for_retry_after_accepted': markForRetryAfterAccepted, + if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt, + if (retryCount != null) 'retry_count': retryCount, + if (lastRetry != null) 'last_retry': lastRetry, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceiptsCompanion copyWith({ + Value? receiptId, + Value? contactId, + Value? messageId, + Value? message, + Value? contactWillSendsReceipt, + Value? willBeRetriedByMediaUpload, + Value? markForRetry, + Value? markForRetryAfterAccepted, + Value? ackByServerAt, + Value? retryCount, + Value? lastRetry, + Value? createdAt, + Value? rowid, + }) { + return ReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId ?? this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + willBeRetriedByMediaUpload: + willBeRetriedByMediaUpload ?? this.willBeRetriedByMediaUpload, + markForRetry: markForRetry ?? this.markForRetry, + markForRetryAfterAccepted: + markForRetryAfterAccepted ?? this.markForRetryAfterAccepted, + ackByServerAt: ackByServerAt ?? this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry ?? this.lastRetry, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (message.present) { + map['message'] = Variable(message.value); + } + if (contactWillSendsReceipt.present) { + map['contact_will_sends_receipt'] = Variable( + contactWillSendsReceipt.value, + ); + } + if (willBeRetriedByMediaUpload.present) { + map['will_be_retried_by_media_upload'] = Variable( + willBeRetriedByMediaUpload.value, + ); + } + if (markForRetry.present) { + map['mark_for_retry'] = Variable(markForRetry.value); + } + if (markForRetryAfterAccepted.present) { + map['mark_for_retry_after_accepted'] = Variable( + markForRetryAfterAccepted.value, + ); + } + if (ackByServerAt.present) { + map['ack_by_server_at'] = Variable(ackByServerAt.value); + } + if (retryCount.present) { + map['retry_count'] = Variable(retryCount.value); + } + if (lastRetry.present) { + map['last_retry'] = Variable(lastRetry.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('willBeRetriedByMediaUpload: $willBeRetriedByMediaUpload, ') + ..write('markForRetry: $markForRetry, ') + ..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ReceivedReceipts extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ReceivedReceipts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [receiptId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'received_receipts'; + @override + Set get $primaryKey => {receiptId}; + @override + ReceivedReceiptsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceivedReceiptsData( + receiptId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}receipt_id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + ReceivedReceipts createAlias(String alias) { + return ReceivedReceipts(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(receipt_id)']; + @override + bool get dontWriteConstraints => true; +} + +class ReceivedReceiptsData extends DataClass + implements Insertable { + final String receiptId; + final int createdAt; + const ReceivedReceiptsData({ + required this.receiptId, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['created_at'] = Variable(createdAt); + return map; + } + + ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceivedReceiptsCompanion( + receiptId: Value(receiptId), + createdAt: Value(createdAt), + ); + } + + factory ReceivedReceiptsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceivedReceiptsData( + receiptId: serializer.fromJson(json['receiptId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReceivedReceiptsData copyWith({String? receiptId, int? createdAt}) => + ReceivedReceiptsData( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + ); + ReceivedReceiptsData copyWithCompanion(ReceivedReceiptsCompanion data) { + return ReceivedReceiptsData( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsData(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(receiptId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceivedReceiptsData && + other.receiptId == this.receiptId && + other.createdAt == this.createdAt); +} + +class ReceivedReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value createdAt; + final Value rowid; + const ReceivedReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceivedReceiptsCompanion.insert({ + required String receiptId, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId); + static Insertable custom({ + Expression? receiptId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceivedReceiptsCompanion copyWith({ + Value? receiptId, + Value? createdAt, + Value? rowid, + }) { + return ReceivedReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalIdentityKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalIdentityKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn identityKey = + GeneratedColumn( + 'identity_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + deviceId, + name, + identityKey, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_identity_key_stores'; + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalIdentityKeyStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalIdentityKeyStoresData( + deviceId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}device_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + identityKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}identity_key'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalIdentityKeyStores createAlias(String alias) { + return SignalIdentityKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(device_id, name)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalIdentityKeyStoresData extends DataClass + implements Insertable { + final int deviceId; + final String name; + final i2.Uint8List identityKey; + final int createdAt; + const SignalIdentityKeyStoresData({ + required this.deviceId, + required this.name, + required this.identityKey, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['identity_key'] = Variable(identityKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalIdentityKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalIdentityKeyStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + identityKey: Value(identityKey), + createdAt: Value(createdAt), + ); + } + + factory SignalIdentityKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalIdentityKeyStoresData( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + identityKey: serializer.fromJson(json['identityKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'identityKey': serializer.toJson(identityKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalIdentityKeyStoresData copyWith({ + int? deviceId, + String? name, + i2.Uint8List? identityKey, + int? createdAt, + }) => SignalIdentityKeyStoresData( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalIdentityKeyStoresData copyWithCompanion( + SignalIdentityKeyStoresCompanion data, + ) { + return SignalIdentityKeyStoresData( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + identityKey: data.identityKey.present + ? data.identityKey.value + : this.identityKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresData(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, + name, + $driftBlobEquality.hash(identityKey), + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalIdentityKeyStoresData && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.identityKey, this.identityKey) && + other.createdAt == this.createdAt); +} + +class SignalIdentityKeyStoresCompanion + extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value identityKey; + final Value createdAt; + final Value rowid; + const SignalIdentityKeyStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.identityKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalIdentityKeyStoresCompanion.insert({ + required int deviceId, + required String name, + required i2.Uint8List identityKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + identityKey = Value(identityKey); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? identityKey, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (identityKey != null) 'identity_key': identityKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalIdentityKeyStoresCompanion copyWith({ + Value? deviceId, + Value? name, + Value? identityKey, + Value? createdAt, + Value? rowid, + }) { + return SignalIdentityKeyStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (identityKey.present) { + map['identity_key'] = Variable(identityKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalPreKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalPreKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn preKeyId = GeneratedColumn( + 'pre_key_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn preKey = + GeneratedColumn( + 'pre_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_pre_key_stores'; + @override + Set get $primaryKey => {preKeyId}; + @override + SignalPreKeyStoresData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalPreKeyStoresData( + preKeyId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pre_key_id'], + )!, + preKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}pre_key'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalPreKeyStores createAlias(String alias) { + return SignalPreKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(pre_key_id)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalPreKeyStoresData extends DataClass + implements Insertable { + final int preKeyId; + final i2.Uint8List preKey; + final int createdAt; + const SignalPreKeyStoresData({ + required this.preKeyId, + required this.preKey, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['pre_key_id'] = Variable(preKeyId); + map['pre_key'] = Variable(preKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalPreKeyStoresCompanion( + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalPreKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalPreKeyStoresData( + preKeyId: serializer.fromJson(json['preKeyId']), + preKey: serializer.fromJson(json['preKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'preKeyId': serializer.toJson(preKeyId), + 'preKey': serializer.toJson(preKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalPreKeyStoresData copyWith({ + int? preKeyId, + i2.Uint8List? preKey, + int? createdAt, + }) => SignalPreKeyStoresData( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalPreKeyStoresData copyWithCompanion(SignalPreKeyStoresCompanion data) { + return SignalPreKeyStoresData( + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresData(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalPreKeyStoresData && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalPreKeyStoresCompanion + extends UpdateCompanion { + final Value preKeyId; + final Value preKey; + final Value createdAt; + const SignalPreKeyStoresCompanion({ + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalPreKeyStoresCompanion.insert({ + this.preKeyId = const Value.absent(), + required i2.Uint8List preKey, + this.createdAt = const Value.absent(), + }) : preKey = Value(preKey); + static Insertable custom({ + Expression? preKeyId, + Expression? preKey, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalPreKeyStoresCompanion copyWith({ + Value? preKeyId, + Value? preKey, + Value? createdAt, + }) { + return SignalPreKeyStoresCompanion( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (preKeyId.present) { + map['pre_key_id'] = Variable(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresCompanion(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class SignalSenderKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSenderKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn senderKeyName = GeneratedColumn( + 'sender_key_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn senderKey = + GeneratedColumn( + 'sender_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [senderKeyName, senderKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_sender_key_stores'; + @override + Set get $primaryKey => {senderKeyName}; + @override + SignalSenderKeyStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSenderKeyStoresData( + senderKeyName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sender_key_name'], + )!, + senderKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}sender_key'], + )!, + ); + } + + @override + SignalSenderKeyStores createAlias(String alias) { + return SignalSenderKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(sender_key_name)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalSenderKeyStoresData extends DataClass + implements Insertable { + final String senderKeyName; + final i2.Uint8List senderKey; + const SignalSenderKeyStoresData({ + required this.senderKeyName, + required this.senderKey, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['sender_key_name'] = Variable(senderKeyName); + map['sender_key'] = Variable(senderKey); + return map; + } + + SignalSenderKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSenderKeyStoresCompanion( + senderKeyName: Value(senderKeyName), + senderKey: Value(senderKey), + ); + } + + factory SignalSenderKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSenderKeyStoresData( + senderKeyName: serializer.fromJson(json['senderKeyName']), + senderKey: serializer.fromJson(json['senderKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'senderKeyName': serializer.toJson(senderKeyName), + 'senderKey': serializer.toJson(senderKey), + }; + } + + SignalSenderKeyStoresData copyWith({ + String? senderKeyName, + i2.Uint8List? senderKey, + }) => SignalSenderKeyStoresData( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + ); + SignalSenderKeyStoresData copyWithCompanion( + SignalSenderKeyStoresCompanion data, + ) { + return SignalSenderKeyStoresData( + senderKeyName: data.senderKeyName.present + ? data.senderKeyName.value + : this.senderKeyName, + senderKey: data.senderKey.present ? data.senderKey.value : this.senderKey, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresData(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(senderKeyName, $driftBlobEquality.hash(senderKey)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSenderKeyStoresData && + other.senderKeyName == this.senderKeyName && + $driftBlobEquality.equals(other.senderKey, this.senderKey)); +} + +class SignalSenderKeyStoresCompanion + extends UpdateCompanion { + final Value senderKeyName; + final Value senderKey; + final Value rowid; + const SignalSenderKeyStoresCompanion({ + this.senderKeyName = const Value.absent(), + this.senderKey = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSenderKeyStoresCompanion.insert({ + required String senderKeyName, + required i2.Uint8List senderKey, + this.rowid = const Value.absent(), + }) : senderKeyName = Value(senderKeyName), + senderKey = Value(senderKey); + static Insertable custom({ + Expression? senderKeyName, + Expression? senderKey, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (senderKeyName != null) 'sender_key_name': senderKeyName, + if (senderKey != null) 'sender_key': senderKey, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSenderKeyStoresCompanion copyWith({ + Value? senderKeyName, + Value? senderKey, + Value? rowid, + }) { + return SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (senderKeyName.present) { + map['sender_key_name'] = Variable(senderKeyName.value); + } + if (senderKey.present) { + map['sender_key'] = Variable(senderKey.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresCompanion(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalSessionStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSessionStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sessionRecord = + GeneratedColumn( + 'session_record', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + deviceId, + name, + sessionRecord, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_session_stores'; + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalSessionStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSessionStoresData( + deviceId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}device_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + sessionRecord: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}session_record'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalSessionStores createAlias(String alias) { + return SignalSessionStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(device_id, name)']; + @override + bool get dontWriteConstraints => true; +} + +class SignalSessionStoresData extends DataClass + implements Insertable { + final int deviceId; + final String name; + final i2.Uint8List sessionRecord; + final int createdAt; + const SignalSessionStoresData({ + required this.deviceId, + required this.name, + required this.sessionRecord, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['session_record'] = Variable(sessionRecord); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalSessionStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSessionStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + sessionRecord: Value(sessionRecord), + createdAt: Value(createdAt), + ); + } + + factory SignalSessionStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSessionStoresData( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + sessionRecord: serializer.fromJson(json['sessionRecord']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'sessionRecord': serializer.toJson(sessionRecord), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalSessionStoresData copyWith({ + int? deviceId, + String? name, + i2.Uint8List? sessionRecord, + int? createdAt, + }) => SignalSessionStoresData( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + ); + SignalSessionStoresData copyWithCompanion(SignalSessionStoresCompanion data) { + return SignalSessionStoresData( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + sessionRecord: data.sessionRecord.present + ? data.sessionRecord.value + : this.sessionRecord, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresData(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, + name, + $driftBlobEquality.hash(sessionRecord), + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSessionStoresData && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.sessionRecord, this.sessionRecord) && + other.createdAt == this.createdAt); +} + +class SignalSessionStoresCompanion + extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value sessionRecord; + final Value createdAt; + final Value rowid; + const SignalSessionStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.sessionRecord = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSessionStoresCompanion.insert({ + required int deviceId, + required String name, + required i2.Uint8List sessionRecord, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + sessionRecord = Value(sessionRecord); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? sessionRecord, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (sessionRecord != null) 'session_record': sessionRecord, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSessionStoresCompanion copyWith({ + Value? deviceId, + Value? name, + Value? sessionRecord, + Value? createdAt, + Value? rowid, + }) { + return SignalSessionStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (sessionRecord.present) { + map['session_record'] = Variable(sessionRecord.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class SignalSignedPreKeyStores extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + SignalSignedPreKeyStores(this.attachedDatabase, [this._alias]); + late final GeneratedColumn signedPreKeyId = GeneratedColumn( + 'signed_pre_key_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn signedPreKey = + GeneratedColumn( + 'signed_pre_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + signedPreKeyId, + signedPreKey, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_signed_pre_key_stores'; + @override + Set get $primaryKey => {signedPreKeyId}; + @override + SignalSignedPreKeyStoresData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSignedPreKeyStoresData( + signedPreKeyId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}signed_pre_key_id'], + )!, + signedPreKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}signed_pre_key'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + SignalSignedPreKeyStores createAlias(String alias) { + return SignalSignedPreKeyStores(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(signed_pre_key_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class SignalSignedPreKeyStoresData extends DataClass + implements Insertable { + final int signedPreKeyId; + final i2.Uint8List signedPreKey; + final int createdAt; + const SignalSignedPreKeyStoresData({ + required this.signedPreKeyId, + required this.signedPreKey, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['signed_pre_key_id'] = Variable(signedPreKeyId); + map['signed_pre_key'] = Variable(signedPreKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalSignedPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSignedPreKeyStoresCompanion( + signedPreKeyId: Value(signedPreKeyId), + signedPreKey: Value(signedPreKey), + createdAt: Value(createdAt), + ); + } + + factory SignalSignedPreKeyStoresData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSignedPreKeyStoresData( + signedPreKeyId: serializer.fromJson(json['signedPreKeyId']), + signedPreKey: serializer.fromJson(json['signedPreKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'signedPreKeyId': serializer.toJson(signedPreKeyId), + 'signedPreKey': serializer.toJson(signedPreKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalSignedPreKeyStoresData copyWith({ + int? signedPreKeyId, + i2.Uint8List? signedPreKey, + int? createdAt, + }) => SignalSignedPreKeyStoresData( + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalSignedPreKeyStoresData copyWithCompanion( + SignalSignedPreKeyStoresCompanion data, + ) { + return SignalSignedPreKeyStoresData( + signedPreKeyId: data.signedPreKeyId.present + ? data.signedPreKeyId.value + : this.signedPreKeyId, + signedPreKey: data.signedPreKey.present + ? data.signedPreKey.value + : this.signedPreKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSignedPreKeyStoresData(') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + signedPreKeyId, + $driftBlobEquality.hash(signedPreKey), + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSignedPreKeyStoresData && + other.signedPreKeyId == this.signedPreKeyId && + $driftBlobEquality.equals(other.signedPreKey, this.signedPreKey) && + other.createdAt == this.createdAt); +} + +class SignalSignedPreKeyStoresCompanion + extends UpdateCompanion { + final Value signedPreKeyId; + final Value signedPreKey; + final Value createdAt; + const SignalSignedPreKeyStoresCompanion({ + this.signedPreKeyId = const Value.absent(), + this.signedPreKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalSignedPreKeyStoresCompanion.insert({ + this.signedPreKeyId = const Value.absent(), + required i2.Uint8List signedPreKey, + this.createdAt = const Value.absent(), + }) : signedPreKey = Value(signedPreKey); + static Insertable custom({ + Expression? signedPreKeyId, + Expression? signedPreKey, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (signedPreKeyId != null) 'signed_pre_key_id': signedPreKeyId, + if (signedPreKey != null) 'signed_pre_key': signedPreKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalSignedPreKeyStoresCompanion copyWith({ + Value? signedPreKeyId, + Value? signedPreKey, + Value? createdAt, + }) { + return SignalSignedPreKeyStoresCompanion( + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (signedPreKeyId.present) { + map['signed_pre_key_id'] = Variable(signedPreKeyId.value); + } + if (signedPreKey.present) { + map['signed_pre_key'] = Variable(signedPreKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSignedPreKeyStoresCompanion(') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class MessageActions extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MessageActions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES messages(message_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [messageId, contactId, type, actionAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_actions'; + @override + Set get $primaryKey => {messageId, contactId, type}; + @override + MessageActionsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageActionsData( + messageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}message_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + actionAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}action_at'], + )!, + ); + } + + @override + MessageActions createAlias(String alias) { + return MessageActions(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(message_id, contact_id, type)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class MessageActionsData extends DataClass + implements Insertable { + final String messageId; + final int contactId; + final String type; + final int actionAt; + const MessageActionsData({ + required this.messageId, + required this.contactId, + required this.type, + required this.actionAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['contact_id'] = Variable(contactId); + map['type'] = Variable(type); + map['action_at'] = Variable(actionAt); + return map; + } + + MessageActionsCompanion toCompanion(bool nullToAbsent) { + return MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory MessageActionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageActionsData( + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + type: serializer.fromJson(json['type']), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'type': serializer.toJson(type), + 'actionAt': serializer.toJson(actionAt), + }; + } + + MessageActionsData copyWith({ + String? messageId, + int? contactId, + String? type, + int? actionAt, + }) => MessageActionsData( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + MessageActionsData copyWithCompanion(MessageActionsCompanion data) { + return MessageActionsData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageActionsData(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, contactId, type, actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageActionsData && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class MessageActionsCompanion extends UpdateCompanion { + final Value messageId; + final Value contactId; + final Value type; + final Value actionAt; + final Value rowid; + const MessageActionsCompanion({ + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageActionsCompanion.insert({ + required String messageId, + required int contactId, + required String type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + contactId = Value(contactId), + type = Value(type); + static Insertable custom({ + Expression? messageId, + Expression? contactId, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageActionsCompanion copyWith({ + Value? messageId, + Value? contactId, + Value? type, + Value? actionAt, + Value? rowid, + }) { + return MessageActionsCompanion( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageActionsCompanion(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class GroupHistories extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + GroupHistories(this.attachedDatabase, [this._alias]); + late final GeneratedColumn groupHistoryId = GeneratedColumn( + 'group_history_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)', + ); + late final GeneratedColumn affectedContactId = GeneratedColumn( + 'affected_contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn oldGroupName = GeneratedColumn( + 'old_group_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn newGroupName = GeneratedColumn( + 'new_group_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn newDeleteMessagesAfterMilliseconds = + GeneratedColumn( + 'new_delete_messages_after_milliseconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_histories'; + @override + Set get $primaryKey => {groupHistoryId}; + @override + GroupHistoriesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupHistoriesData( + groupHistoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_history_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + ), + affectedContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}affected_contact_id'], + ), + oldGroupName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}old_group_name'], + ), + newGroupName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}new_group_name'], + ), + newDeleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}new_delete_messages_after_milliseconds'], + ), + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + actionAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}action_at'], + )!, + ); + } + + @override + GroupHistories createAlias(String alias) { + return GroupHistories(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(group_history_id)']; + @override + bool get dontWriteConstraints => true; +} + +class GroupHistoriesData extends DataClass + implements Insertable { + final String groupHistoryId; + final String groupId; + final int? contactId; + final int? affectedContactId; + final String? oldGroupName; + final String? newGroupName; + final int? newDeleteMessagesAfterMilliseconds; + final String type; + final int actionAt; + const GroupHistoriesData({ + required this.groupHistoryId, + required this.groupId, + this.contactId, + this.affectedContactId, + this.oldGroupName, + this.newGroupName, + this.newDeleteMessagesAfterMilliseconds, + required this.type, + required this.actionAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_history_id'] = Variable(groupHistoryId); + map['group_id'] = Variable(groupId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + if (!nullToAbsent || affectedContactId != null) { + map['affected_contact_id'] = Variable(affectedContactId); + } + if (!nullToAbsent || oldGroupName != null) { + map['old_group_name'] = Variable(oldGroupName); + } + if (!nullToAbsent || newGroupName != null) { + map['new_group_name'] = Variable(newGroupName); + } + if (!nullToAbsent || newDeleteMessagesAfterMilliseconds != null) { + map['new_delete_messages_after_milliseconds'] = Variable( + newDeleteMessagesAfterMilliseconds, + ); + } + map['type'] = Variable(type); + map['action_at'] = Variable(actionAt); + return map; + } + + GroupHistoriesCompanion toCompanion(bool nullToAbsent) { + return GroupHistoriesCompanion( + groupHistoryId: Value(groupHistoryId), + groupId: Value(groupId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + affectedContactId: affectedContactId == null && nullToAbsent + ? const Value.absent() + : Value(affectedContactId), + oldGroupName: oldGroupName == null && nullToAbsent + ? const Value.absent() + : Value(oldGroupName), + newGroupName: newGroupName == null && nullToAbsent + ? const Value.absent() + : Value(newGroupName), + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(newDeleteMessagesAfterMilliseconds), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory GroupHistoriesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupHistoriesData( + groupHistoryId: serializer.fromJson(json['groupHistoryId']), + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + affectedContactId: serializer.fromJson(json['affectedContactId']), + oldGroupName: serializer.fromJson(json['oldGroupName']), + newGroupName: serializer.fromJson(json['newGroupName']), + newDeleteMessagesAfterMilliseconds: serializer.fromJson( + json['newDeleteMessagesAfterMilliseconds'], + ), + type: serializer.fromJson(json['type']), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupHistoryId': serializer.toJson(groupHistoryId), + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'affectedContactId': serializer.toJson(affectedContactId), + 'oldGroupName': serializer.toJson(oldGroupName), + 'newGroupName': serializer.toJson(newGroupName), + 'newDeleteMessagesAfterMilliseconds': serializer.toJson( + newDeleteMessagesAfterMilliseconds, + ), + 'type': serializer.toJson(type), + 'actionAt': serializer.toJson(actionAt), + }; + } + + GroupHistoriesData copyWith({ + String? groupHistoryId, + String? groupId, + Value contactId = const Value.absent(), + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = const Value.absent(), + String? type, + int? actionAt, + }) => GroupHistoriesData( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId.present ? contactId.value : this.contactId, + affectedContactId: affectedContactId.present + ? affectedContactId.value + : this.affectedContactId, + oldGroupName: oldGroupName.present ? oldGroupName.value : this.oldGroupName, + newGroupName: newGroupName.present ? newGroupName.value : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds.present + ? newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + GroupHistoriesData copyWithCompanion(GroupHistoriesCompanion data) { + return GroupHistoriesData( + groupHistoryId: data.groupHistoryId.present + ? data.groupHistoryId.value + : this.groupHistoryId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + affectedContactId: data.affectedContactId.present + ? data.affectedContactId.value + : this.affectedContactId, + oldGroupName: data.oldGroupName.present + ? data.oldGroupName.value + : this.oldGroupName, + newGroupName: data.newGroupName.present + ? data.newGroupName.value + : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + data.newDeleteMessagesAfterMilliseconds.present + ? data.newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesData(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ', + ) + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupHistoriesData && + other.groupHistoryId == this.groupHistoryId && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.affectedContactId == this.affectedContactId && + other.oldGroupName == this.oldGroupName && + other.newGroupName == this.newGroupName && + other.newDeleteMessagesAfterMilliseconds == + this.newDeleteMessagesAfterMilliseconds && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class GroupHistoriesCompanion extends UpdateCompanion { + final Value groupHistoryId; + final Value groupId; + final Value contactId; + final Value affectedContactId; + final Value oldGroupName; + final Value newGroupName; + final Value newDeleteMessagesAfterMilliseconds; + final Value type; + final Value actionAt; + final Value rowid; + const GroupHistoriesCompanion({ + this.groupHistoryId = const Value.absent(), + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupHistoriesCompanion.insert({ + required String groupHistoryId, + required String groupId, + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + required String type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupHistoryId = Value(groupHistoryId), + groupId = Value(groupId), + type = Value(type); + static Insertable custom({ + Expression? groupHistoryId, + Expression? groupId, + Expression? contactId, + Expression? affectedContactId, + Expression? oldGroupName, + Expression? newGroupName, + Expression? newDeleteMessagesAfterMilliseconds, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupHistoryId != null) 'group_history_id': groupHistoryId, + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (affectedContactId != null) 'affected_contact_id': affectedContactId, + if (oldGroupName != null) 'old_group_name': oldGroupName, + if (newGroupName != null) 'new_group_name': newGroupName, + if (newDeleteMessagesAfterMilliseconds != null) + 'new_delete_messages_after_milliseconds': + newDeleteMessagesAfterMilliseconds, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupHistoriesCompanion copyWith({ + Value? groupHistoryId, + Value? groupId, + Value? contactId, + Value? affectedContactId, + Value? oldGroupName, + Value? newGroupName, + Value? newDeleteMessagesAfterMilliseconds, + Value? type, + Value? actionAt, + Value? rowid, + }) { + return GroupHistoriesCompanion( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + affectedContactId: affectedContactId ?? this.affectedContactId, + oldGroupName: oldGroupName ?? this.oldGroupName, + newGroupName: newGroupName ?? this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds ?? + this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupHistoryId.present) { + map['group_history_id'] = Variable(groupHistoryId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (affectedContactId.present) { + map['affected_contact_id'] = Variable(affectedContactId.value); + } + if (oldGroupName.present) { + map['old_group_name'] = Variable(oldGroupName.value); + } + if (newGroupName.present) { + map['new_group_name'] = Variable(newGroupName.value); + } + if (newDeleteMessagesAfterMilliseconds.present) { + map['new_delete_messages_after_milliseconds'] = Variable( + newDeleteMessagesAfterMilliseconds.value, + ); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesCompanion(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ', + ) + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class KeyVerifications extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + KeyVerifications(this.attachedDatabase, [this._alias]); + late final GeneratedColumn verificationId = GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn verifiedBy = GeneratedColumn( + 'verified_by', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [ + verificationId, + contactId, + type, + verifiedBy, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'key_verifications'; + @override + Set get $primaryKey => {verificationId}; + @override + KeyVerificationsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return KeyVerificationsData( + verificationId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verification_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + verifiedBy: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verified_by'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + KeyVerifications createAlias(String alias) { + return KeyVerifications(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class KeyVerificationsData extends DataClass + implements Insertable { + final int verificationId; + final int contactId; + final String type; + final int? verifiedBy; + final int createdAt; + const KeyVerificationsData({ + required this.verificationId, + required this.contactId, + required this.type, + this.verifiedBy, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['verification_id'] = Variable(verificationId); + map['contact_id'] = Variable(contactId); + map['type'] = Variable(type); + if (!nullToAbsent || verifiedBy != null) { + map['verified_by'] = Variable(verifiedBy); + } + map['created_at'] = Variable(createdAt); + return map; + } + + KeyVerificationsCompanion toCompanion(bool nullToAbsent) { + return KeyVerificationsCompanion( + verificationId: Value(verificationId), + contactId: Value(contactId), + type: Value(type), + verifiedBy: verifiedBy == null && nullToAbsent + ? const Value.absent() + : Value(verifiedBy), + createdAt: Value(createdAt), + ); + } + + factory KeyVerificationsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return KeyVerificationsData( + verificationId: serializer.fromJson(json['verificationId']), + contactId: serializer.fromJson(json['contactId']), + type: serializer.fromJson(json['type']), + verifiedBy: serializer.fromJson(json['verifiedBy']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'verificationId': serializer.toJson(verificationId), + 'contactId': serializer.toJson(contactId), + 'type': serializer.toJson(type), + 'verifiedBy': serializer.toJson(verifiedBy), + 'createdAt': serializer.toJson(createdAt), + }; + } + + KeyVerificationsData copyWith({ + int? verificationId, + int? contactId, + String? type, + Value verifiedBy = const Value.absent(), + int? createdAt, + }) => KeyVerificationsData( + verificationId: verificationId ?? this.verificationId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + verifiedBy: verifiedBy.present ? verifiedBy.value : this.verifiedBy, + createdAt: createdAt ?? this.createdAt, + ); + KeyVerificationsData copyWithCompanion(KeyVerificationsCompanion data) { + return KeyVerificationsData( + verificationId: data.verificationId.present + ? data.verificationId.value + : this.verificationId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + verifiedBy: data.verifiedBy.present + ? data.verifiedBy.value + : this.verifiedBy, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('KeyVerificationsData(') + ..write('verificationId: $verificationId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('verifiedBy: $verifiedBy, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(verificationId, contactId, type, verifiedBy, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is KeyVerificationsData && + other.verificationId == this.verificationId && + other.contactId == this.contactId && + other.type == this.type && + other.verifiedBy == this.verifiedBy && + other.createdAt == this.createdAt); +} + +class KeyVerificationsCompanion extends UpdateCompanion { + final Value verificationId; + final Value contactId; + final Value type; + final Value verifiedBy; + final Value createdAt; + const KeyVerificationsCompanion({ + this.verificationId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.verifiedBy = const Value.absent(), + this.createdAt = const Value.absent(), + }); + KeyVerificationsCompanion.insert({ + this.verificationId = const Value.absent(), + required int contactId, + required String type, + this.verifiedBy = const Value.absent(), + this.createdAt = const Value.absent(), + }) : contactId = Value(contactId), + type = Value(type); + static Insertable custom({ + Expression? verificationId, + Expression? contactId, + Expression? type, + Expression? verifiedBy, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (verificationId != null) 'verification_id': verificationId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (verifiedBy != null) 'verified_by': verifiedBy, + if (createdAt != null) 'created_at': createdAt, + }); + } + + KeyVerificationsCompanion copyWith({ + Value? verificationId, + Value? contactId, + Value? type, + Value? verifiedBy, + Value? createdAt, + }) { + return KeyVerificationsCompanion( + verificationId: verificationId ?? this.verificationId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + verifiedBy: verifiedBy ?? this.verifiedBy, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (verificationId.present) { + map['verification_id'] = Variable(verificationId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (verifiedBy.present) { + map['verified_by'] = Variable(verifiedBy.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('KeyVerificationsCompanion(') + ..write('verificationId: $verificationId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('verifiedBy: $verifiedBy, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class VerificationTokens extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + VerificationTokens(this.attachedDatabase, [this._alias]); + late final GeneratedColumn tokenId = GeneratedColumn( + 'token_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn token = + GeneratedColumn( + 'token', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT (CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER))', + defaultValue: const CustomExpression( + 'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)', + ), + ); + @override + List get $columns => [tokenId, token, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'verification_tokens'; + @override + Set get $primaryKey => {tokenId}; + @override + VerificationTokensData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return VerificationTokensData( + tokenId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}token_id'], + )!, + token: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}token'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + VerificationTokens createAlias(String alias) { + return VerificationTokens(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class VerificationTokensData extends DataClass + implements Insertable { + final int tokenId; + final i2.Uint8List token; + final int createdAt; + const VerificationTokensData({ + required this.tokenId, + required this.token, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['token_id'] = Variable(tokenId); + map['token'] = Variable(token); + map['created_at'] = Variable(createdAt); + return map; + } + + VerificationTokensCompanion toCompanion(bool nullToAbsent) { + return VerificationTokensCompanion( + tokenId: Value(tokenId), + token: Value(token), + createdAt: Value(createdAt), + ); + } + + factory VerificationTokensData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return VerificationTokensData( + tokenId: serializer.fromJson(json['tokenId']), + token: serializer.fromJson(json['token']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'tokenId': serializer.toJson(tokenId), + 'token': serializer.toJson(token), + 'createdAt': serializer.toJson(createdAt), + }; + } + + VerificationTokensData copyWith({ + int? tokenId, + i2.Uint8List? token, + int? createdAt, + }) => VerificationTokensData( + tokenId: tokenId ?? this.tokenId, + token: token ?? this.token, + createdAt: createdAt ?? this.createdAt, + ); + VerificationTokensData copyWithCompanion(VerificationTokensCompanion data) { + return VerificationTokensData( + tokenId: data.tokenId.present ? data.tokenId.value : this.tokenId, + token: data.token.present ? data.token.value : this.token, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('VerificationTokensData(') + ..write('tokenId: $tokenId, ') + ..write('token: $token, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(tokenId, $driftBlobEquality.hash(token), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is VerificationTokensData && + other.tokenId == this.tokenId && + $driftBlobEquality.equals(other.token, this.token) && + other.createdAt == this.createdAt); +} + +class VerificationTokensCompanion + extends UpdateCompanion { + final Value tokenId; + final Value token; + final Value createdAt; + const VerificationTokensCompanion({ + this.tokenId = const Value.absent(), + this.token = const Value.absent(), + this.createdAt = const Value.absent(), + }); + VerificationTokensCompanion.insert({ + this.tokenId = const Value.absent(), + required i2.Uint8List token, + this.createdAt = const Value.absent(), + }) : token = Value(token); + static Insertable custom({ + Expression? tokenId, + Expression? token, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (tokenId != null) 'token_id': tokenId, + if (token != null) 'token': token, + if (createdAt != null) 'created_at': createdAt, + }); + } + + VerificationTokensCompanion copyWith({ + Value? tokenId, + Value? token, + Value? createdAt, + }) { + return VerificationTokensCompanion( + tokenId: tokenId ?? this.tokenId, + token: token ?? this.token, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (tokenId.present) { + map['token_id'] = Variable(tokenId.value); + } + if (token.present) { + map['token'] = Variable(token.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('VerificationTokensCompanion(') + ..write('tokenId: $tokenId, ') + ..write('token: $token, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryAnnouncedUsers extends Table + with + TableInfo< + UserDiscoveryAnnouncedUsers, + UserDiscoveryAnnouncedUsersData + > { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryAnnouncedUsers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn announcedUserId = GeneratedColumn( + 'announced_user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn announcedPublicKey = + GeneratedColumn( + 'announced_public_key', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicId = GeneratedColumn( + 'public_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE', + ); + late final GeneratedColumn username = GeneratedColumn( + 'username', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn wasShownToTheUser = GeneratedColumn( + 'was_shown_to_the_user', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn wasAskedFriends = GeneratedColumn( + 'was_asked_friends', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: + 'NOT NULL DEFAULT 0 CHECK (was_asked_friends IN (0, 1))', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + announcedUserId, + announcedPublicKey, + publicId, + username, + wasShownToTheUser, + isHidden, + wasAskedFriends, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_announced_users'; + @override + Set get $primaryKey => {announcedUserId}; + @override + UserDiscoveryAnnouncedUsersData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryAnnouncedUsersData( + announcedUserId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}announced_user_id'], + )!, + announcedPublicKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}announced_public_key'], + )!, + publicId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_id'], + )!, + username: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}username'], + ), + wasShownToTheUser: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}was_shown_to_the_user'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}is_hidden'], + )!, + wasAskedFriends: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}was_asked_friends'], + )!, + ); + } + + @override + UserDiscoveryAnnouncedUsers createAlias(String alias) { + return UserDiscoveryAnnouncedUsers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(announced_user_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryAnnouncedUsersData extends DataClass + implements Insertable { + final int announcedUserId; + final i2.Uint8List announcedPublicKey; + final int publicId; + final String? username; + final int wasShownToTheUser; + final int isHidden; + final int wasAskedFriends; + const UserDiscoveryAnnouncedUsersData({ + required this.announcedUserId, + required this.announcedPublicKey, + required this.publicId, + this.username, + required this.wasShownToTheUser, + required this.isHidden, + required this.wasAskedFriends, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['announced_user_id'] = Variable(announcedUserId); + map['announced_public_key'] = Variable(announcedPublicKey); + map['public_id'] = Variable(publicId); + if (!nullToAbsent || username != null) { + map['username'] = Variable(username); + } + map['was_shown_to_the_user'] = Variable(wasShownToTheUser); + map['is_hidden'] = Variable(isHidden); + map['was_asked_friends'] = Variable(wasAskedFriends); + return map; + } + + UserDiscoveryAnnouncedUsersCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryAnnouncedUsersCompanion( + announcedUserId: Value(announcedUserId), + announcedPublicKey: Value(announcedPublicKey), + publicId: Value(publicId), + username: username == null && nullToAbsent + ? const Value.absent() + : Value(username), + wasShownToTheUser: Value(wasShownToTheUser), + isHidden: Value(isHidden), + wasAskedFriends: Value(wasAskedFriends), + ); + } + + factory UserDiscoveryAnnouncedUsersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryAnnouncedUsersData( + announcedUserId: serializer.fromJson(json['announcedUserId']), + announcedPublicKey: serializer.fromJson( + json['announcedPublicKey'], + ), + publicId: serializer.fromJson(json['publicId']), + username: serializer.fromJson(json['username']), + wasShownToTheUser: serializer.fromJson(json['wasShownToTheUser']), + isHidden: serializer.fromJson(json['isHidden']), + wasAskedFriends: serializer.fromJson(json['wasAskedFriends']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'announcedUserId': serializer.toJson(announcedUserId), + 'announcedPublicKey': serializer.toJson(announcedPublicKey), + 'publicId': serializer.toJson(publicId), + 'username': serializer.toJson(username), + 'wasShownToTheUser': serializer.toJson(wasShownToTheUser), + 'isHidden': serializer.toJson(isHidden), + 'wasAskedFriends': serializer.toJson(wasAskedFriends), + }; + } + + UserDiscoveryAnnouncedUsersData copyWith({ + int? announcedUserId, + i2.Uint8List? announcedPublicKey, + int? publicId, + Value username = const Value.absent(), + int? wasShownToTheUser, + int? isHidden, + int? wasAskedFriends, + }) => UserDiscoveryAnnouncedUsersData( + announcedUserId: announcedUserId ?? this.announcedUserId, + announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, + publicId: publicId ?? this.publicId, + username: username.present ? username.value : this.username, + wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, + isHidden: isHidden ?? this.isHidden, + wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends, + ); + UserDiscoveryAnnouncedUsersData copyWithCompanion( + UserDiscoveryAnnouncedUsersCompanion data, + ) { + return UserDiscoveryAnnouncedUsersData( + announcedUserId: data.announcedUserId.present + ? data.announcedUserId.value + : this.announcedUserId, + announcedPublicKey: data.announcedPublicKey.present + ? data.announcedPublicKey.value + : this.announcedPublicKey, + publicId: data.publicId.present ? data.publicId.value : this.publicId, + username: data.username.present ? data.username.value : this.username, + wasShownToTheUser: data.wasShownToTheUser.present + ? data.wasShownToTheUser.value + : this.wasShownToTheUser, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + wasAskedFriends: data.wasAskedFriends.present + ? data.wasAskedFriends.value + : this.wasAskedFriends, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryAnnouncedUsersData(') + ..write('announcedUserId: $announcedUserId, ') + ..write('announcedPublicKey: $announcedPublicKey, ') + ..write('publicId: $publicId, ') + ..write('username: $username, ') + ..write('wasShownToTheUser: $wasShownToTheUser, ') + ..write('isHidden: $isHidden, ') + ..write('wasAskedFriends: $wasAskedFriends') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + announcedUserId, + $driftBlobEquality.hash(announcedPublicKey), + publicId, + username, + wasShownToTheUser, + isHidden, + wasAskedFriends, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryAnnouncedUsersData && + other.announcedUserId == this.announcedUserId && + $driftBlobEquality.equals( + other.announcedPublicKey, + this.announcedPublicKey, + ) && + other.publicId == this.publicId && + other.username == this.username && + other.wasShownToTheUser == this.wasShownToTheUser && + other.isHidden == this.isHidden && + other.wasAskedFriends == this.wasAskedFriends); +} + +class UserDiscoveryAnnouncedUsersCompanion + extends UpdateCompanion { + final Value announcedUserId; + final Value announcedPublicKey; + final Value publicId; + final Value username; + final Value wasShownToTheUser; + final Value isHidden; + final Value wasAskedFriends; + const UserDiscoveryAnnouncedUsersCompanion({ + this.announcedUserId = const Value.absent(), + this.announcedPublicKey = const Value.absent(), + this.publicId = const Value.absent(), + this.username = const Value.absent(), + this.wasShownToTheUser = const Value.absent(), + this.isHidden = const Value.absent(), + this.wasAskedFriends = const Value.absent(), + }); + UserDiscoveryAnnouncedUsersCompanion.insert({ + this.announcedUserId = const Value.absent(), + required i2.Uint8List announcedPublicKey, + required int publicId, + this.username = const Value.absent(), + this.wasShownToTheUser = const Value.absent(), + this.isHidden = const Value.absent(), + this.wasAskedFriends = const Value.absent(), + }) : announcedPublicKey = Value(announcedPublicKey), + publicId = Value(publicId); + static Insertable custom({ + Expression? announcedUserId, + Expression? announcedPublicKey, + Expression? publicId, + Expression? username, + Expression? wasShownToTheUser, + Expression? isHidden, + Expression? wasAskedFriends, + }) { + return RawValuesInsertable({ + if (announcedUserId != null) 'announced_user_id': announcedUserId, + if (announcedPublicKey != null) + 'announced_public_key': announcedPublicKey, + if (publicId != null) 'public_id': publicId, + if (username != null) 'username': username, + if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser, + if (isHidden != null) 'is_hidden': isHidden, + if (wasAskedFriends != null) 'was_asked_friends': wasAskedFriends, + }); + } + + UserDiscoveryAnnouncedUsersCompanion copyWith({ + Value? announcedUserId, + Value? announcedPublicKey, + Value? publicId, + Value? username, + Value? wasShownToTheUser, + Value? isHidden, + Value? wasAskedFriends, + }) { + return UserDiscoveryAnnouncedUsersCompanion( + announcedUserId: announcedUserId ?? this.announcedUserId, + announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, + publicId: publicId ?? this.publicId, + username: username ?? this.username, + wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, + isHidden: isHidden ?? this.isHidden, + wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (announcedUserId.present) { + map['announced_user_id'] = Variable(announcedUserId.value); + } + if (announcedPublicKey.present) { + map['announced_public_key'] = Variable( + announcedPublicKey.value, + ); + } + if (publicId.present) { + map['public_id'] = Variable(publicId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (wasShownToTheUser.present) { + map['was_shown_to_the_user'] = Variable(wasShownToTheUser.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (wasAskedFriends.present) { + map['was_asked_friends'] = Variable(wasAskedFriends.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryAnnouncedUsersCompanion(') + ..write('announcedUserId: $announcedUserId, ') + ..write('announcedPublicKey: $announcedPublicKey, ') + ..write('publicId: $publicId, ') + ..write('username: $username, ') + ..write('wasShownToTheUser: $wasShownToTheUser, ') + ..write('isHidden: $isHidden, ') + ..write('wasAskedFriends: $wasAskedFriends') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryUserRelations extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryUserRelations(this.attachedDatabase, [this._alias]); + late final GeneratedColumn announcedUserId = GeneratedColumn( + 'announced_user_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES user_discovery_announced_users(announced_user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn fromContactId = GeneratedColumn( + 'from_contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn publicKeyVerifiedTimestamp = + GeneratedColumn( + 'public_key_verified_timestamp', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + announcedUserId, + fromContactId, + publicKeyVerifiedTimestamp, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_user_relations'; + @override + Set get $primaryKey => {announcedUserId, fromContactId}; + @override + UserDiscoveryUserRelationsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryUserRelationsData( + announcedUserId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}announced_user_id'], + )!, + fromContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}from_contact_id'], + )!, + publicKeyVerifiedTimestamp: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_key_verified_timestamp'], + ), + ); + } + + @override + UserDiscoveryUserRelations createAlias(String alias) { + return UserDiscoveryUserRelations(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(announced_user_id, from_contact_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryUserRelationsData extends DataClass + implements Insertable { + final int announcedUserId; + final int fromContactId; + final int? publicKeyVerifiedTimestamp; + const UserDiscoveryUserRelationsData({ + required this.announcedUserId, + required this.fromContactId, + this.publicKeyVerifiedTimestamp, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['announced_user_id'] = Variable(announcedUserId); + map['from_contact_id'] = Variable(fromContactId); + if (!nullToAbsent || publicKeyVerifiedTimestamp != null) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp, + ); + } + return map; + } + + UserDiscoveryUserRelationsCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryUserRelationsCompanion( + announcedUserId: Value(announcedUserId), + fromContactId: Value(fromContactId), + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp == null && nullToAbsent + ? const Value.absent() + : Value(publicKeyVerifiedTimestamp), + ); + } + + factory UserDiscoveryUserRelationsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryUserRelationsData( + announcedUserId: serializer.fromJson(json['announcedUserId']), + fromContactId: serializer.fromJson(json['fromContactId']), + publicKeyVerifiedTimestamp: serializer.fromJson( + json['publicKeyVerifiedTimestamp'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'announcedUserId': serializer.toJson(announcedUserId), + 'fromContactId': serializer.toJson(fromContactId), + 'publicKeyVerifiedTimestamp': serializer.toJson( + publicKeyVerifiedTimestamp, + ), + }; + } + + UserDiscoveryUserRelationsData copyWith({ + int? announcedUserId, + int? fromContactId, + Value publicKeyVerifiedTimestamp = const Value.absent(), + }) => UserDiscoveryUserRelationsData( + announcedUserId: announcedUserId ?? this.announcedUserId, + fromContactId: fromContactId ?? this.fromContactId, + publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp.present + ? publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + UserDiscoveryUserRelationsData copyWithCompanion( + UserDiscoveryUserRelationsCompanion data, + ) { + return UserDiscoveryUserRelationsData( + announcedUserId: data.announcedUserId.present + ? data.announcedUserId.value + : this.announcedUserId, + fromContactId: data.fromContactId.present + ? data.fromContactId.value + : this.fromContactId, + publicKeyVerifiedTimestamp: data.publicKeyVerifiedTimestamp.present + ? data.publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryUserRelationsData(') + ..write('announcedUserId: $announcedUserId, ') + ..write('fromContactId: $fromContactId, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(announcedUserId, fromContactId, publicKeyVerifiedTimestamp); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryUserRelationsData && + other.announcedUserId == this.announcedUserId && + other.fromContactId == this.fromContactId && + other.publicKeyVerifiedTimestamp == this.publicKeyVerifiedTimestamp); +} + +class UserDiscoveryUserRelationsCompanion + extends UpdateCompanion { + final Value announcedUserId; + final Value fromContactId; + final Value publicKeyVerifiedTimestamp; + final Value rowid; + const UserDiscoveryUserRelationsCompanion({ + this.announcedUserId = const Value.absent(), + this.fromContactId = const Value.absent(), + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }); + UserDiscoveryUserRelationsCompanion.insert({ + required int announcedUserId, + required int fromContactId, + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }) : announcedUserId = Value(announcedUserId), + fromContactId = Value(fromContactId); + static Insertable custom({ + Expression? announcedUserId, + Expression? fromContactId, + Expression? publicKeyVerifiedTimestamp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (announcedUserId != null) 'announced_user_id': announcedUserId, + if (fromContactId != null) 'from_contact_id': fromContactId, + if (publicKeyVerifiedTimestamp != null) + 'public_key_verified_timestamp': publicKeyVerifiedTimestamp, + if (rowid != null) 'rowid': rowid, + }); + } + + UserDiscoveryUserRelationsCompanion copyWith({ + Value? announcedUserId, + Value? fromContactId, + Value? publicKeyVerifiedTimestamp, + Value? rowid, + }) { + return UserDiscoveryUserRelationsCompanion( + announcedUserId: announcedUserId ?? this.announcedUserId, + fromContactId: fromContactId ?? this.fromContactId, + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp ?? this.publicKeyVerifiedTimestamp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (announcedUserId.present) { + map['announced_user_id'] = Variable(announcedUserId.value); + } + if (fromContactId.present) { + map['from_contact_id'] = Variable(fromContactId.value); + } + if (publicKeyVerifiedTimestamp.present) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp.value, + ); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryUserRelationsCompanion(') + ..write('announcedUserId: $announcedUserId, ') + ..write('fromContactId: $fromContactId, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryOtherPromotions extends Table + with + TableInfo< + UserDiscoveryOtherPromotions, + UserDiscoveryOtherPromotionsData + > { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryOtherPromotions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn fromContactId = GeneratedColumn( + 'from_contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn promotionId = GeneratedColumn( + 'promotion_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicId = GeneratedColumn( + 'public_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn threshold = GeneratedColumn( + 'threshold', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn announcementShare = + GeneratedColumn( + 'announcement_share', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn publicKeyVerifiedTimestamp = + GeneratedColumn( + 'public_key_verified_timestamp', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + @override + List get $columns => [ + fromContactId, + promotionId, + publicId, + threshold, + announcementShare, + publicKeyVerifiedTimestamp, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_other_promotions'; + @override + Set get $primaryKey => {fromContactId, publicId}; + @override + UserDiscoveryOtherPromotionsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryOtherPromotionsData( + fromContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}from_contact_id'], + )!, + promotionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}promotion_id'], + )!, + publicId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_id'], + )!, + threshold: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}threshold'], + )!, + announcementShare: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}announcement_share'], + )!, + publicKeyVerifiedTimestamp: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}public_key_verified_timestamp'], + ), + ); + } + + @override + UserDiscoveryOtherPromotions createAlias(String alias) { + return UserDiscoveryOtherPromotions(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(from_contact_id, public_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryOtherPromotionsData extends DataClass + implements Insertable { + final int fromContactId; + final int promotionId; + final int publicId; + final int threshold; + final i2.Uint8List announcementShare; + final int? publicKeyVerifiedTimestamp; + const UserDiscoveryOtherPromotionsData({ + required this.fromContactId, + required this.promotionId, + required this.publicId, + required this.threshold, + required this.announcementShare, + this.publicKeyVerifiedTimestamp, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['from_contact_id'] = Variable(fromContactId); + map['promotion_id'] = Variable(promotionId); + map['public_id'] = Variable(publicId); + map['threshold'] = Variable(threshold); + map['announcement_share'] = Variable(announcementShare); + if (!nullToAbsent || publicKeyVerifiedTimestamp != null) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp, + ); + } + return map; + } + + UserDiscoveryOtherPromotionsCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryOtherPromotionsCompanion( + fromContactId: Value(fromContactId), + promotionId: Value(promotionId), + publicId: Value(publicId), + threshold: Value(threshold), + announcementShare: Value(announcementShare), + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp == null && nullToAbsent + ? const Value.absent() + : Value(publicKeyVerifiedTimestamp), + ); + } + + factory UserDiscoveryOtherPromotionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryOtherPromotionsData( + fromContactId: serializer.fromJson(json['fromContactId']), + promotionId: serializer.fromJson(json['promotionId']), + publicId: serializer.fromJson(json['publicId']), + threshold: serializer.fromJson(json['threshold']), + announcementShare: serializer.fromJson( + json['announcementShare'], + ), + publicKeyVerifiedTimestamp: serializer.fromJson( + json['publicKeyVerifiedTimestamp'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'fromContactId': serializer.toJson(fromContactId), + 'promotionId': serializer.toJson(promotionId), + 'publicId': serializer.toJson(publicId), + 'threshold': serializer.toJson(threshold), + 'announcementShare': serializer.toJson(announcementShare), + 'publicKeyVerifiedTimestamp': serializer.toJson( + publicKeyVerifiedTimestamp, + ), + }; + } + + UserDiscoveryOtherPromotionsData copyWith({ + int? fromContactId, + int? promotionId, + int? publicId, + int? threshold, + i2.Uint8List? announcementShare, + Value publicKeyVerifiedTimestamp = const Value.absent(), + }) => UserDiscoveryOtherPromotionsData( + fromContactId: fromContactId ?? this.fromContactId, + promotionId: promotionId ?? this.promotionId, + publicId: publicId ?? this.publicId, + threshold: threshold ?? this.threshold, + announcementShare: announcementShare ?? this.announcementShare, + publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp.present + ? publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + UserDiscoveryOtherPromotionsData copyWithCompanion( + UserDiscoveryOtherPromotionsCompanion data, + ) { + return UserDiscoveryOtherPromotionsData( + fromContactId: data.fromContactId.present + ? data.fromContactId.value + : this.fromContactId, + promotionId: data.promotionId.present + ? data.promotionId.value + : this.promotionId, + publicId: data.publicId.present ? data.publicId.value : this.publicId, + threshold: data.threshold.present ? data.threshold.value : this.threshold, + announcementShare: data.announcementShare.present + ? data.announcementShare.value + : this.announcementShare, + publicKeyVerifiedTimestamp: data.publicKeyVerifiedTimestamp.present + ? data.publicKeyVerifiedTimestamp.value + : this.publicKeyVerifiedTimestamp, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOtherPromotionsData(') + ..write('fromContactId: $fromContactId, ') + ..write('promotionId: $promotionId, ') + ..write('publicId: $publicId, ') + ..write('threshold: $threshold, ') + ..write('announcementShare: $announcementShare, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + fromContactId, + promotionId, + publicId, + threshold, + $driftBlobEquality.hash(announcementShare), + publicKeyVerifiedTimestamp, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryOtherPromotionsData && + other.fromContactId == this.fromContactId && + other.promotionId == this.promotionId && + other.publicId == this.publicId && + other.threshold == this.threshold && + $driftBlobEquality.equals( + other.announcementShare, + this.announcementShare, + ) && + other.publicKeyVerifiedTimestamp == this.publicKeyVerifiedTimestamp); +} + +class UserDiscoveryOtherPromotionsCompanion + extends UpdateCompanion { + final Value fromContactId; + final Value promotionId; + final Value publicId; + final Value threshold; + final Value announcementShare; + final Value publicKeyVerifiedTimestamp; + final Value rowid; + const UserDiscoveryOtherPromotionsCompanion({ + this.fromContactId = const Value.absent(), + this.promotionId = const Value.absent(), + this.publicId = const Value.absent(), + this.threshold = const Value.absent(), + this.announcementShare = const Value.absent(), + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }); + UserDiscoveryOtherPromotionsCompanion.insert({ + required int fromContactId, + required int promotionId, + required int publicId, + required int threshold, + required i2.Uint8List announcementShare, + this.publicKeyVerifiedTimestamp = const Value.absent(), + this.rowid = const Value.absent(), + }) : fromContactId = Value(fromContactId), + promotionId = Value(promotionId), + publicId = Value(publicId), + threshold = Value(threshold), + announcementShare = Value(announcementShare); + static Insertable custom({ + Expression? fromContactId, + Expression? promotionId, + Expression? publicId, + Expression? threshold, + Expression? announcementShare, + Expression? publicKeyVerifiedTimestamp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (fromContactId != null) 'from_contact_id': fromContactId, + if (promotionId != null) 'promotion_id': promotionId, + if (publicId != null) 'public_id': publicId, + if (threshold != null) 'threshold': threshold, + if (announcementShare != null) 'announcement_share': announcementShare, + if (publicKeyVerifiedTimestamp != null) + 'public_key_verified_timestamp': publicKeyVerifiedTimestamp, + if (rowid != null) 'rowid': rowid, + }); + } + + UserDiscoveryOtherPromotionsCompanion copyWith({ + Value? fromContactId, + Value? promotionId, + Value? publicId, + Value? threshold, + Value? announcementShare, + Value? publicKeyVerifiedTimestamp, + Value? rowid, + }) { + return UserDiscoveryOtherPromotionsCompanion( + fromContactId: fromContactId ?? this.fromContactId, + promotionId: promotionId ?? this.promotionId, + publicId: publicId ?? this.publicId, + threshold: threshold ?? this.threshold, + announcementShare: announcementShare ?? this.announcementShare, + publicKeyVerifiedTimestamp: + publicKeyVerifiedTimestamp ?? this.publicKeyVerifiedTimestamp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (fromContactId.present) { + map['from_contact_id'] = Variable(fromContactId.value); + } + if (promotionId.present) { + map['promotion_id'] = Variable(promotionId.value); + } + if (publicId.present) { + map['public_id'] = Variable(publicId.value); + } + if (threshold.present) { + map['threshold'] = Variable(threshold.value); + } + if (announcementShare.present) { + map['announcement_share'] = Variable( + announcementShare.value, + ); + } + if (publicKeyVerifiedTimestamp.present) { + map['public_key_verified_timestamp'] = Variable( + publicKeyVerifiedTimestamp.value, + ); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOtherPromotionsCompanion(') + ..write('fromContactId: $fromContactId, ') + ..write('promotionId: $promotionId, ') + ..write('publicId: $publicId, ') + ..write('threshold: $threshold, ') + ..write('announcementShare: $announcementShare, ') + ..write('publicKeyVerifiedTimestamp: $publicKeyVerifiedTimestamp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryOwnPromotions extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryOwnPromotions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn versionId = GeneratedColumn( + 'version_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + late final GeneratedColumn promotion = + GeneratedColumn( + 'promotion', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [versionId, contactId, promotion]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_own_promotions'; + @override + Set get $primaryKey => {versionId}; + @override + UserDiscoveryOwnPromotionsData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoveryOwnPromotionsData( + versionId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}version_id'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + )!, + promotion: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}promotion'], + )!, + ); + } + + @override + UserDiscoveryOwnPromotions createAlias(String alias) { + return UserDiscoveryOwnPromotions(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoveryOwnPromotionsData extends DataClass + implements Insertable { + final int versionId; + final int contactId; + final i2.Uint8List promotion; + const UserDiscoveryOwnPromotionsData({ + required this.versionId, + required this.contactId, + required this.promotion, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['version_id'] = Variable(versionId); + map['contact_id'] = Variable(contactId); + map['promotion'] = Variable(promotion); + return map; + } + + UserDiscoveryOwnPromotionsCompanion toCompanion(bool nullToAbsent) { + return UserDiscoveryOwnPromotionsCompanion( + versionId: Value(versionId), + contactId: Value(contactId), + promotion: Value(promotion), + ); + } + + factory UserDiscoveryOwnPromotionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoveryOwnPromotionsData( + versionId: serializer.fromJson(json['versionId']), + contactId: serializer.fromJson(json['contactId']), + promotion: serializer.fromJson(json['promotion']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'versionId': serializer.toJson(versionId), + 'contactId': serializer.toJson(contactId), + 'promotion': serializer.toJson(promotion), + }; + } + + UserDiscoveryOwnPromotionsData copyWith({ + int? versionId, + int? contactId, + i2.Uint8List? promotion, + }) => UserDiscoveryOwnPromotionsData( + versionId: versionId ?? this.versionId, + contactId: contactId ?? this.contactId, + promotion: promotion ?? this.promotion, + ); + UserDiscoveryOwnPromotionsData copyWithCompanion( + UserDiscoveryOwnPromotionsCompanion data, + ) { + return UserDiscoveryOwnPromotionsData( + versionId: data.versionId.present ? data.versionId.value : this.versionId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + promotion: data.promotion.present ? data.promotion.value : this.promotion, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOwnPromotionsData(') + ..write('versionId: $versionId, ') + ..write('contactId: $contactId, ') + ..write('promotion: $promotion') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(versionId, contactId, $driftBlobEquality.hash(promotion)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoveryOwnPromotionsData && + other.versionId == this.versionId && + other.contactId == this.contactId && + $driftBlobEquality.equals(other.promotion, this.promotion)); +} + +class UserDiscoveryOwnPromotionsCompanion + extends UpdateCompanion { + final Value versionId; + final Value contactId; + final Value promotion; + const UserDiscoveryOwnPromotionsCompanion({ + this.versionId = const Value.absent(), + this.contactId = const Value.absent(), + this.promotion = const Value.absent(), + }); + UserDiscoveryOwnPromotionsCompanion.insert({ + this.versionId = const Value.absent(), + required int contactId, + required i2.Uint8List promotion, + }) : contactId = Value(contactId), + promotion = Value(promotion); + static Insertable custom({ + Expression? versionId, + Expression? contactId, + Expression? promotion, + }) { + return RawValuesInsertable({ + if (versionId != null) 'version_id': versionId, + if (contactId != null) 'contact_id': contactId, + if (promotion != null) 'promotion': promotion, + }); + } + + UserDiscoveryOwnPromotionsCompanion copyWith({ + Value? versionId, + Value? contactId, + Value? promotion, + }) { + return UserDiscoveryOwnPromotionsCompanion( + versionId: versionId ?? this.versionId, + contactId: contactId ?? this.contactId, + promotion: promotion ?? this.promotion, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (versionId.present) { + map['version_id'] = Variable(versionId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (promotion.present) { + map['promotion'] = Variable(promotion.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoveryOwnPromotionsCompanion(') + ..write('versionId: $versionId, ') + ..write('contactId: $contactId, ') + ..write('promotion: $promotion') + ..write(')')) + .toString(); + } +} + +class UserDiscoveryShares extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserDiscoveryShares(this.attachedDatabase, [this._alias]); + late final GeneratedColumn shareId = GeneratedColumn( + 'share_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn share = + GeneratedColumn( + 'share', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL REFERENCES contacts(user_id)ON DELETE CASCADE', + ); + @override + List get $columns => [shareId, share, contactId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_discovery_shares'; + @override + Set get $primaryKey => {shareId}; + @override + UserDiscoverySharesData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserDiscoverySharesData( + shareId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}share_id'], + )!, + share: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}share'], + )!, + contactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}contact_id'], + ), + ); + } + + @override + UserDiscoveryShares createAlias(String alias) { + return UserDiscoveryShares(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class UserDiscoverySharesData extends DataClass + implements Insertable { + final int shareId; + final i2.Uint8List share; + final int? contactId; + const UserDiscoverySharesData({ + required this.shareId, + required this.share, + this.contactId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['share_id'] = Variable(shareId); + map['share'] = Variable(share); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + return map; + } + + UserDiscoverySharesCompanion toCompanion(bool nullToAbsent) { + return UserDiscoverySharesCompanion( + shareId: Value(shareId), + share: Value(share), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + ); + } + + factory UserDiscoverySharesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserDiscoverySharesData( + shareId: serializer.fromJson(json['shareId']), + share: serializer.fromJson(json['share']), + contactId: serializer.fromJson(json['contactId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shareId': serializer.toJson(shareId), + 'share': serializer.toJson(share), + 'contactId': serializer.toJson(contactId), + }; + } + + UserDiscoverySharesData copyWith({ + int? shareId, + i2.Uint8List? share, + Value contactId = const Value.absent(), + }) => UserDiscoverySharesData( + shareId: shareId ?? this.shareId, + share: share ?? this.share, + contactId: contactId.present ? contactId.value : this.contactId, + ); + UserDiscoverySharesData copyWithCompanion(UserDiscoverySharesCompanion data) { + return UserDiscoverySharesData( + shareId: data.shareId.present ? data.shareId.value : this.shareId, + share: data.share.present ? data.share.value : this.share, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + ); + } + + @override + String toString() { + return (StringBuffer('UserDiscoverySharesData(') + ..write('shareId: $shareId, ') + ..write('share: $share, ') + ..write('contactId: $contactId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(shareId, $driftBlobEquality.hash(share), contactId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserDiscoverySharesData && + other.shareId == this.shareId && + $driftBlobEquality.equals(other.share, this.share) && + other.contactId == this.contactId); +} + +class UserDiscoverySharesCompanion + extends UpdateCompanion { + final Value shareId; + final Value share; + final Value contactId; + const UserDiscoverySharesCompanion({ + this.shareId = const Value.absent(), + this.share = const Value.absent(), + this.contactId = const Value.absent(), + }); + UserDiscoverySharesCompanion.insert({ + this.shareId = const Value.absent(), + required i2.Uint8List share, + this.contactId = const Value.absent(), + }) : share = Value(share); + static Insertable custom({ + Expression? shareId, + Expression? share, + Expression? contactId, + }) { + return RawValuesInsertable({ + if (shareId != null) 'share_id': shareId, + if (share != null) 'share': share, + if (contactId != null) 'contact_id': contactId, + }); + } + + UserDiscoverySharesCompanion copyWith({ + Value? shareId, + Value? share, + Value? contactId, + }) { + return UserDiscoverySharesCompanion( + shareId: shareId ?? this.shareId, + share: share ?? this.share, + contactId: contactId ?? this.contactId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shareId.present) { + map['share_id'] = Variable(shareId.value); + } + if (share.present) { + map['share'] = Variable(share.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserDiscoverySharesCompanion(') + ..write('shareId: $shareId, ') + ..write('share: $share, ') + ..write('contactId: $contactId') + ..write(')')) + .toString(); + } +} + +class Shortcuts extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Shortcuts(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL UNIQUE', + ); + late final GeneratedColumn usageCounter = GeneratedColumn( + 'usage_counter', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [id, emoji, usageCounter]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcuts'; + @override + Set get $primaryKey => {id}; + @override + ShortcutsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + emoji: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}emoji'], + )!, + usageCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}usage_counter'], + )!, + ); + } + + @override + Shortcuts createAlias(String alias) { + return Shortcuts(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ShortcutsData extends DataClass implements Insertable { + final int id; + final String emoji; + final int usageCounter; + const ShortcutsData({ + required this.id, + required this.emoji, + required this.usageCounter, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['emoji'] = Variable(emoji); + map['usage_counter'] = Variable(usageCounter); + return map; + } + + ShortcutsCompanion toCompanion(bool nullToAbsent) { + return ShortcutsCompanion( + id: Value(id), + emoji: Value(emoji), + usageCounter: Value(usageCounter), + ); + } + + factory ShortcutsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutsData( + id: serializer.fromJson(json['id']), + emoji: serializer.fromJson(json['emoji']), + usageCounter: serializer.fromJson(json['usageCounter']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'emoji': serializer.toJson(emoji), + 'usageCounter': serializer.toJson(usageCounter), + }; + } + + ShortcutsData copyWith({int? id, String? emoji, int? usageCounter}) => + ShortcutsData( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + ShortcutsData copyWithCompanion(ShortcutsCompanion data) { + return ShortcutsData( + id: data.id.present ? data.id.value : this.id, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + usageCounter: data.usageCounter.present + ? data.usageCounter.value + : this.usageCounter, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutsData(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, emoji, usageCounter); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutsData && + other.id == this.id && + other.emoji == this.emoji && + other.usageCounter == this.usageCounter); +} + +class ShortcutsCompanion extends UpdateCompanion { + final Value id; + final Value emoji; + final Value usageCounter; + const ShortcutsCompanion({ + this.id = const Value.absent(), + this.emoji = const Value.absent(), + this.usageCounter = const Value.absent(), + }); + ShortcutsCompanion.insert({ + this.id = const Value.absent(), + required String emoji, + this.usageCounter = const Value.absent(), + }) : emoji = Value(emoji); + static Insertable custom({ + Expression? id, + Expression? emoji, + Expression? usageCounter, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (emoji != null) 'emoji': emoji, + if (usageCounter != null) 'usage_counter': usageCounter, + }); + } + + ShortcutsCompanion copyWith({ + Value? id, + Value? emoji, + Value? usageCounter, + }) { + return ShortcutsCompanion( + id: id ?? this.id, + emoji: emoji ?? this.emoji, + usageCounter: usageCounter ?? this.usageCounter, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (usageCounter.present) { + map['usage_counter'] = Variable(usageCounter.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutsCompanion(') + ..write('id: $id, ') + ..write('emoji: $emoji, ') + ..write('usageCounter: $usageCounter') + ..write(')')) + .toString(); + } +} + +class ShortcutMembers extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ShortcutMembers(this.attachedDatabase, [this._alias]); + late final GeneratedColumn shortcutId = GeneratedColumn( + 'shortcut_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES shortcuts(id)ON DELETE CASCADE', + ); + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: + 'NOT NULL REFERENCES "groups"(group_id)ON DELETE CASCADE', + ); + @override + List get $columns => [shortcutId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'shortcut_members'; + @override + Set get $primaryKey => {shortcutId, groupId}; + @override + ShortcutMembersData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ShortcutMembersData( + shortcutId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}shortcut_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + ShortcutMembers createAlias(String alias) { + return ShortcutMembers(attachedDatabase, alias); + } + + @override + List get customConstraints => const [ + 'PRIMARY KEY(shortcut_id, group_id)', + ]; + @override + bool get dontWriteConstraints => true; +} + +class ShortcutMembersData extends DataClass + implements Insertable { + final int shortcutId; + final String groupId; + const ShortcutMembersData({required this.shortcutId, required this.groupId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shortcut_id'] = Variable(shortcutId); + map['group_id'] = Variable(groupId); + return map; + } + + ShortcutMembersCompanion toCompanion(bool nullToAbsent) { + return ShortcutMembersCompanion( + shortcutId: Value(shortcutId), + groupId: Value(groupId), + ); + } + + factory ShortcutMembersData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ShortcutMembersData( + shortcutId: serializer.fromJson(json['shortcutId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'shortcutId': serializer.toJson(shortcutId), + 'groupId': serializer.toJson(groupId), + }; + } + + ShortcutMembersData copyWith({int? shortcutId, String? groupId}) => + ShortcutMembersData( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + ); + ShortcutMembersData copyWithCompanion(ShortcutMembersCompanion data) { + return ShortcutMembersData( + shortcutId: data.shortcutId.present + ? data.shortcutId.value + : this.shortcutId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersData(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(shortcutId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ShortcutMembersData && + other.shortcutId == this.shortcutId && + other.groupId == this.groupId); +} + +class ShortcutMembersCompanion extends UpdateCompanion { + final Value shortcutId; + final Value groupId; + final Value rowid; + const ShortcutMembersCompanion({ + this.shortcutId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + ShortcutMembersCompanion.insert({ + required int shortcutId, + required String groupId, + this.rowid = const Value.absent(), + }) : shortcutId = Value(shortcutId), + groupId = Value(groupId); + static Insertable custom({ + Expression? shortcutId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (shortcutId != null) 'shortcut_id': shortcutId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + ShortcutMembersCompanion copyWith({ + Value? shortcutId, + Value? groupId, + Value? rowid, + }) { + return ShortcutMembersCompanion( + shortcutId: shortcutId ?? this.shortcutId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (shortcutId.present) { + map['shortcut_id'] = Variable(shortcutId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ShortcutMembersCompanion(') + ..write('shortcutId: $shortcutId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV23 extends GeneratedDatabase { + DatabaseAtV23(QueryExecutor e) : super(e); + late final Contacts contacts = Contacts(this); + late final Groups groups = Groups(this); + late final MediaFiles mediaFiles = MediaFiles(this); + late final Messages messages = Messages(this); + late final MessageHistories messageHistories = MessageHistories(this); + late final Reactions reactions = Reactions(this); + late final GroupMembers groupMembers = GroupMembers(this); + late final Receipts receipts = Receipts(this); + late final ReceivedReceipts receivedReceipts = ReceivedReceipts(this); + late final SignalIdentityKeyStores signalIdentityKeyStores = + SignalIdentityKeyStores(this); + late final SignalPreKeyStores signalPreKeyStores = SignalPreKeyStores(this); + late final SignalSenderKeyStores signalSenderKeyStores = + SignalSenderKeyStores(this); + late final SignalSessionStores signalSessionStores = SignalSessionStores( + this, + ); + late final SignalSignedPreKeyStores signalSignedPreKeyStores = + SignalSignedPreKeyStores(this); + late final MessageActions messageActions = MessageActions(this); + late final GroupHistories groupHistories = GroupHistories(this); + late final KeyVerifications keyVerifications = KeyVerifications(this); + late final VerificationTokens verificationTokens = VerificationTokens(this); + late final UserDiscoveryAnnouncedUsers userDiscoveryAnnouncedUsers = + UserDiscoveryAnnouncedUsers(this); + late final UserDiscoveryUserRelations userDiscoveryUserRelations = + UserDiscoveryUserRelations(this); + late final UserDiscoveryOtherPromotions userDiscoveryOtherPromotions = + UserDiscoveryOtherPromotions(this); + late final UserDiscoveryOwnPromotions userDiscoveryOwnPromotions = + UserDiscoveryOwnPromotions(this); + late final UserDiscoveryShares userDiscoveryShares = UserDiscoveryShares( + this, + ); + late final Shortcuts shortcuts = Shortcuts(this); + late final ShortcutMembers shortcutMembers = ShortcutMembers(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalSignedPreKeyStores, + messageActions, + groupHistories, + keyVerifications, + verificationTokens, + userDiscoveryAnnouncedUsers, + userDiscoveryUserRelations, + userDiscoveryOtherPromotions, + userDiscoveryOwnPromotions, + userDiscoveryShares, + shortcuts, + shortcutMembers, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('messages', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'media_files', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('messages', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_histories', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_histories', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('reactions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('reactions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('group_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('receipts', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('receipts', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'messages', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_actions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('message_actions', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('group_histories', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('key_verifications', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('key_verifications', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'user_discovery_announced_users', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_user_relations', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_user_relations', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_other_promotions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('user_discovery_own_promotions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'contacts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('user_discovery_shares', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'shortcuts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'groups', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('shortcut_members', kind: UpdateKind.delete)], + ), + ]); + @override + int get schemaVersion => 23; +} diff --git a/test/services/cloud_backup_test.dart b/test/services/cloud_backup_test.dart new file mode 100644 index 00000000..97b420c7 --- /dev/null +++ b/test/services/cloud_backup_test.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:drift/drift.dart' show Value; +import 'package:flutter/services.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/core/bridge.dart' as bridge; +import 'package:twonly/core/frb_generated.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/api.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/log.dart'; + +import '../mocks/platform_channels.dart'; +import '../mocks/test_client.dart'; + +void main() { + group('Memories Cloud Backup Integration', () { + late TestClient client; + late Directory tempDir; + + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('dev.fluttercommunity.plus/package_info'), + (call) async { + return { + 'appName': 'twonly', + 'packageName': 'eu.twonly.app', + 'version': '1.0.0', + 'buildNumber': '100', + }; + }, + ); + Log.init(); + setupPlatformChannelMocks(); + HttpOverrides.global = RealHttpOverrides(); + final dylibPath = + '${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib'; + if (File(dylibPath).existsSync()) { + await RustLib.init(externalLibrary: ExternalLibrary.open(dylibPath)); + } else { + await RustLib.init(); + } + tempDir = Directory.systemTemp.createTempSync( + 'twonly_cloud_backup_test_', + ); + AppEnvironment.initTesting( + customCacheDir: tempDir.path, + customSupportDir: tempDir.path, + ); + + await bridge.initializeTwonlyFlutter( + config: bridge.InitConfig( + databaseDir: tempDir.path, + dataDir: tempDir.path, + ), + ); + + if (locator.isRegistered()) { + await locator.unregister(); + } + if (locator.isRegistered()) { + await locator.unregister(); + } + if (locator.isRegistered()) { + await locator.unregister(); + } + + locator + ..registerFactory(() { + final db = Zone.current[#twonlyDB] as TwonlyDB?; + if (db != null) return db; + throw StateError('No TwonlyDB in active Zone.'); + }) + ..registerFactory(() { + final us = Zone.current[#userService] as UserService?; + if (us != null) return us; + throw StateError('No UserService in active Zone.'); + }) + ..registerFactory(() { + final api = Zone.current[#apiService] as ApiService?; + if (api != null) return api; + throw StateError('No ApiService in active Zone.'); + }); + + client = TestClient(777); + await client.init(); + }); + + tearDownAll(() async { + try { + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + } catch (_) {} + }); + + test('Full Backup Lifecycle', () async { + await client.run(() async { + // 1. Create a dummy media file + final mediaId = 'test_media_${DateTime.now().millisecondsSinceEpoch}'; + + await twonlyDB.mediaFilesDao.insertOrUpdateMedia( + MediaFilesCompanion( + mediaId: Value(mediaId), + createdAt: Value(DateTime.now()), + stored: const Value(true), + hasThumbnail: const Value(true), + cloudState: const Value(CloudState.none), + type: const Value(MediaType.image), + ), + ); + + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( + mediaId, + ); + expect(mediaFile, isNotNull); + expect(mediaFile!.cloudState, CloudState.none); + + // Mock original and thumbnail files + final mediaService = MediaFileService(mediaFile); + final originalFile = mediaService.originalPath; + final thumbnailFile = mediaService.thumbnailPath; + + await originalFile.create(recursive: true); + await originalFile.writeAsBytes([1, 2, 3, 4, 5]); + + await thumbnailFile.create(recursive: true); + await thumbnailFile.writeAsBytes([1, 2, 3]); + + // 2. Trigger checkUploads (which performs S3 POST upload and confirmation) + await memoriesCloudService.checkUploads(); + + // 3. Verify that the media has cloudState == uploaded + final finalMedia = await twonlyDB.mediaFilesDao.getMediaFileById( + mediaId, + ); + expect(finalMedia, isNotNull); + expect(finalMedia!.cloudState, CloudState.uploaded); + }); + }); + }); +} From 498f5d7b727ada7d615ef52f2a0358b762164a38 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 23:05:50 +0200 Subject: [PATCH 18/29] first poc for e2ee backup --- CHANGELOG.md | 1 + README.md | 6 +- lib/src/database/daos/mediafiles.dao.dart | 27 +- .../generated/app_localizations.dart | 114 ++++++++ .../generated/app_localizations_de.dart | 69 ++++- .../generated/app_localizations_en.dart | 67 +++++ lib/src/localization/translations | 2 +- .../memories/memories_cloud.service.dart | 47 ++- .../delete_memories_dialog.comp.dart | 117 ++++++++ .../components/selectable_thumbnail.comp.dart | 8 +- .../visual/elements/my_button.element.dart | 21 +- .../components/memory_thumbnail.comp.dart | 90 ++++-- .../components/selection_menu.comp.dart | 116 ++++++++ .../components/selection_toolbar.comp.dart | 130 --------- ...chronized_viewer_actions_toolbar.comp.dart | 200 +++++++------ .../synchronized_viewer_item.comp.dart | 162 +++++++++++ .../visual/views/memories/memories.view.dart | 275 +++++++++--------- .../memories/synchronized_viewer.view.dart | 195 +++++-------- .../settings/backup/backup_settings.view.dart | 60 +--- .../data_and_storage/manage_storage.view.dart | 156 +++++++++- 20 files changed, 1277 insertions(+), 586 deletions(-) create mode 100644 lib/src/visual/components/delete_memories_dialog.comp.dart create mode 100644 lib/src/visual/views/memories/components/selection_menu.comp.dart delete mode 100644 lib/src/visual/views/memories/components/selection_toolbar.comp.dart create mode 100644 lib/src/visual/views/memories/components/synchronized_viewer_item.comp.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e6772f04..5e14c284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.4.0 +- New: Encrypted Cloud Backup of Memories - New: Passwordless Backup - Fix: Performance issues diff --git a/README.md b/README.md index c4ba2a2a..024c9eea 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ If you decide to give twonly a try, please keep in mind that it is still in its - Open Source and can be downloaded directly from GitHub - No email or phone number required to register - The backend is hosted exclusively in Europe +- [Passwordless Backup](https://twonly.eu/en/blog/2026-passwordless-backup.html) without a phone number +- [User Discovery](https://twonly.eu/en/blog/2026-mutual-friends.html) without a phone number +- E2EE cloud backup of memories ## Roadmap ### Currently -- Focus on user-friendliness so that people enjoy using the app - - Passwordless recovery without a phone number - Implementation of features so that Snapchat can actually be replaced - - E2EE cloud backup of memories - Importing memories from Snapchat ### Next on the bucket list diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 040faf50..2e5d18dd 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -1,8 +1,11 @@ +import 'dart:io'; + import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; part 'mediafiles.dao.g.dart'; @@ -217,10 +220,30 @@ class MediaFilesDao extends DatabaseAccessor final rows = await select(mediaFiles).get(); final stats = {}; + final Set existingPaths; + if (rows.isNotEmpty) { + final dummyMs = MediaFileService(rows.first); + final storedDir = dummyMs.storedPath.parent; + if (storedDir.existsSync()) { + existingPaths = storedDir + .listSync() + .whereType() + .map((f) => f.path) + .toSet(); + } else { + existingPaths = {}; + } + } else { + existingPaths = {}; + } + for (final row in rows) { final type = row.type; - final size = row.sizeInBytes ?? 0; - stats[type] = (stats[type] ?? 0) + size; + final ms = MediaFileService(row); + if (existingPaths.contains(ms.storedPath.path)) { + final size = row.sizeInBytes ?? 0; + stats[type] = (stats[type] ?? 0) + size; + } } return stats; diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index cde12c55..348d0259 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1442,6 +1442,60 @@ abstract class AppLocalizations { /// **'{count, plural, =1 {The image will be irrevocably deleted.} other {The {count} images will be irrevocably deleted.}}'** String deleteMemoriesBody(num count); + /// No description provided for @deleteMemoriesLocalOnly. + /// + /// In en, this message translates to: + /// **'Local Only'** + String get deleteMemoriesLocalOnly; + + /// No description provided for @deleteMemoriesCompletely. + /// + /// In en, this message translates to: + /// **'Completely'** + String get deleteMemoriesCompletely; + + /// No description provided for @memoriesSelectedCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1 {1 Element} other {{count} Elements}}'** + String memoriesSelectedCount(num count); + + /// No description provided for @memoriesMenuSelectAll. + /// + /// In en, this message translates to: + /// **'Select all'** + String get memoriesMenuSelectAll; + + /// No description provided for @memoriesMenuDeselectAll. + /// + /// In en, this message translates to: + /// **'Deselect all'** + String get memoriesMenuDeselectAll; + + /// No description provided for @memoriesMenuExport. + /// + /// In en, this message translates to: + /// **'Export to gallery'** + String get memoriesMenuExport; + + /// No description provided for @memoriesMenuFavorite. + /// + /// In en, this message translates to: + /// **'Favorite'** + String get memoriesMenuFavorite; + + /// No description provided for @memoriesMenuDelete. + /// + /// In en, this message translates to: + /// **'Delete completely'** + String get memoriesMenuDelete; + + /// No description provided for @memoriesMenuDeleteLocal. + /// + /// In en, this message translates to: + /// **'Delete locally'** + String get memoriesMenuDeleteLocal; + /// No description provided for @settingsBackup. /// /// In en, this message translates to: @@ -4057,6 +4111,66 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Select Contacts'** String get missingRecoveryContactsCardAction; + + /// No description provided for @memoriesBackupLimitReached. + /// + /// In en, this message translates to: + /// **'Cloud backup limit reached! Please upgrade your plan or free up space.'** + String get memoriesBackupLimitReached; + + /// No description provided for @settingsStorageLocal. + /// + /// In en, this message translates to: + /// **'Local storage'** + String get settingsStorageLocal; + + /// No description provided for @settingsStorageNoCloudBackupTitle. + /// + /// In en, this message translates to: + /// **'No Cloud Backup'** + String get settingsStorageNoCloudBackupTitle; + + /// No description provided for @settingsStorageNoCloudBackupCard. + /// + /// In en, this message translates to: + /// **'Back up your memories to the cloud to free up local space and ensure you never lose your pictures. All end-to-end encrypted.'** + String get settingsStorageNoCloudBackupCard; + + /// No description provided for @galleryActionSave. + /// + /// In en, this message translates to: + /// **'Save'** + String get galleryActionSave; + + /// No description provided for @galleryActionExport. + /// + /// In en, this message translates to: + /// **'Export'** + String get galleryActionExport; + + /// No description provided for @galleryActionFavorite. + /// + /// In en, this message translates to: + /// **'Favorite'** + String get galleryActionFavorite; + + /// No description provided for @galleryActionUnfavorite. + /// + /// In en, this message translates to: + /// **'Unfavorite'** + String get galleryActionUnfavorite; + + /// No description provided for @galleryActionDelete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get galleryActionDelete; + + /// No description provided for @galleryActionShare. + /// + /// In en, this message translates to: + /// **'Share'** + String get galleryActionShare; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 31e7cf75..88402311 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -664,7 +664,7 @@ class AppLocalizationsDe extends AppLocalizations { String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer'; @override - String get familyFeature3 => '50GB Memories Speicher'; + String get familyFeature3 => '✓ 50GB Memories Speicher'; @override String get familyFeature4 => '✓ Flammen wiederherstellen'; @@ -758,6 +758,41 @@ class AppLocalizationsDe extends AppLocalizations { return '$_temp0'; } + @override + String get deleteMemoriesLocalOnly => 'Nur lokal'; + + @override + String get deleteMemoriesCompletely => 'Komplett'; + + @override + String memoriesSelectedCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Elemente', + one: '1 Element', + ); + return '$_temp0'; + } + + @override + String get memoriesMenuSelectAll => 'Alles auswählen'; + + @override + String get memoriesMenuDeselectAll => 'Alle abwählen'; + + @override + String get memoriesMenuExport => 'In Gallery exportieren'; + + @override + String get memoriesMenuFavorite => 'Favorisieren'; + + @override + String get memoriesMenuDelete => 'Löschen'; + + @override + String get memoriesMenuDeleteLocal => 'Lokal löschen'; + @override String get settingsBackup => 'Backup'; @@ -2348,4 +2383,36 @@ class AppLocalizationsDe extends AppLocalizations { @override String get missingRecoveryContactsCardAction => 'Kontakte auswählen'; + + @override + String get memoriesBackupLimitReached => + 'Cloud-Backup-Limit erreicht! Bitte aktualisiere dein Abonnement oder gib Speicherplatz frei.'; + + @override + String get settingsStorageLocal => 'Lokaler Speicherplatz'; + + @override + String get settingsStorageNoCloudBackupTitle => 'Kein Cloud-Backup'; + + @override + String get settingsStorageNoCloudBackupCard => + 'Sichere deine Erinnerungen in der Cloud, um lokalen Speicherplatz freizugeben und deine Bilder nicht zu verlieren. Alles Ende-zu-Ende verschlüsselt.'; + + @override + String get galleryActionSave => 'Sichern'; + + @override + String get galleryActionExport => 'Exportieren'; + + @override + String get galleryActionFavorite => 'Favorit'; + + @override + String get galleryActionUnfavorite => 'Favorit'; + + @override + String get galleryActionDelete => 'Löschen'; + + @override + String get galleryActionShare => 'Teilen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 550684a3..1c4ab096 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -753,6 +753,41 @@ class AppLocalizationsEn extends AppLocalizations { return '$_temp0'; } + @override + String get deleteMemoriesLocalOnly => 'Local Only'; + + @override + String get deleteMemoriesCompletely => 'Completely'; + + @override + String memoriesSelectedCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Elements', + one: '1 Element', + ); + return '$_temp0'; + } + + @override + String get memoriesMenuSelectAll => 'Select all'; + + @override + String get memoriesMenuDeselectAll => 'Deselect all'; + + @override + String get memoriesMenuExport => 'Export to gallery'; + + @override + String get memoriesMenuFavorite => 'Favorite'; + + @override + String get memoriesMenuDelete => 'Delete completely'; + + @override + String get memoriesMenuDeleteLocal => 'Delete locally'; + @override String get settingsBackup => 'Backup'; @@ -2327,4 +2362,36 @@ class AppLocalizationsEn extends AppLocalizations { @override String get missingRecoveryContactsCardAction => 'Select Contacts'; + + @override + String get memoriesBackupLimitReached => + 'Cloud backup limit reached! Please upgrade your plan or free up space.'; + + @override + String get settingsStorageLocal => 'Local storage'; + + @override + String get settingsStorageNoCloudBackupTitle => 'No Cloud Backup'; + + @override + String get settingsStorageNoCloudBackupCard => + 'Back up your memories to the cloud to free up local space and ensure you never lose your pictures. All end-to-end encrypted.'; + + @override + String get galleryActionSave => 'Save'; + + @override + String get galleryActionExport => 'Export'; + + @override + String get galleryActionFavorite => 'Favorite'; + + @override + String get galleryActionUnfavorite => 'Unfavorite'; + + @override + String get galleryActionDelete => 'Delete'; + + @override + String get galleryActionShare => 'Share'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 499ad4a7..9d05713a 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 499ad4a7b703c1dff414f531c528894ad0c91799 +Subproject commit 9d05713a1f69e7e4de742c414df7df6c4c7925a7 diff --git a/lib/src/services/memories/memories_cloud.service.dart b/lib/src/services/memories/memories_cloud.service.dart index f5ff0789..fc6c3e21 100644 --- a/lib/src/services/memories/memories_cloud.service.dart +++ b/lib/src/services/memories/memories_cloud.service.dart @@ -6,6 +6,7 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; +import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; @@ -58,6 +59,8 @@ class ProgressMultipartRequest extends http.MultipartRequest { } class MemoriesCloudService { + static final Map _fileMutexes = {}; + Timer? _timer; bool _isProcessing = false; @@ -152,24 +155,38 @@ class MemoriesCloudService { } } - static Future downloadThumbnail(MediaFileService media) async { - final urls = await apiService.getMemoriesUrl(media.mediaFile.mediaId, true); - if (urls == null || !urls.hasFullDownloadUrl()) return false; + static Future downloadFromCloud( + MediaFileService media, { + required bool isThumbnail, + }) async { + final mediaId = media.mediaFile.mediaId; + final mutex = _fileMutexes.putIfAbsent(mediaId, Mutex.new); - try { - final response = await http.get(Uri.parse(urls.fullDownloadUrl)); + return mutex.protect(() async { + final targetPath = isThumbnail ? media.thumbnailPath : media.storedPath; - if (response.statusCode == 200) { - return await _decryptFile(response.bodyBytes, media.thumbnailPath); - } else { - Log.warn( - 'Failed to download thumbnai statuscode ${response.statusCode}', - ); + if (targetPath.existsSync() && targetPath.lengthSync() > 0) { + return true; } - } catch (e) { - Log.warn(e); - } - return false; + + final urls = await apiService.getMemoriesUrl(mediaId, isThumbnail); + if (urls == null || !urls.hasFullDownloadUrl()) return false; + + try { + final response = await http.get(Uri.parse(urls.fullDownloadUrl)); + + if (response.statusCode == 200) { + return await _decryptFile(response.bodyBytes, targetPath); + } else { + Log.warn( + 'Failed to download ${isThumbnail ? 'thumbnail' : 'full media'} statuscode ${response.statusCode}', + ); + } + } catch (e) { + Log.warn(e); + } + return false; + }); } Future _backupMemory( diff --git a/lib/src/visual/components/delete_memories_dialog.comp.dart b/lib/src/visual/components/delete_memories_dialog.comp.dart new file mode 100644 index 00000000..3dd307c3 --- /dev/null +++ b/lib/src/visual/components/delete_memories_dialog.comp.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; + +Future showDeleteMemoriesDialog({ + required BuildContext context, + required int count, + required bool hasCloudBackup, +}) { + return showDialog( + context: context, + builder: (context) => DeleteMemoriesDialog( + count: count, + hasCloudBackup: hasCloudBackup, + ), + ); +} + +class DeleteMemoriesDialog extends StatelessWidget { + const DeleteMemoriesDialog({ + required this.count, + required this.hasCloudBackup, + super.key, + }); + + final int count; + final bool hasCloudBackup; + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.deleteImageTitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 16), + Text( + context.lang.deleteMemoriesBody(count), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + const SizedBox(height: 24), + if (hasCloudBackup) ...[ + Row( + children: [ + Expanded( + child: MyButton( + variant: MyButtonVariant.secondaryMiddle, + onPressed: () => Navigator.pop(context, false), + child: Text(context.lang.deleteMemoriesLocalOnly), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MyButton( + variant: MyButtonVariant.errorMiddle, + onPressed: () => Navigator.pop(context, true), + child: Text(context.lang.deleteMemoriesCompletely), + ), + ), + ], + ), + const SizedBox(height: 8), + MyButton( + variant: MyButtonVariant.text, + onPressed: () => Navigator.pop(context), + child: Text(context.lang.galleryCancel), + ), + ] else ...[ + Row( + children: [ + Expanded( + child: MyButton( + variant: MyButtonVariant.secondaryMiddle, + onPressed: () => Navigator.pop(context), + child: Text(context.lang.galleryCancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MyButton( + variant: MyButtonVariant.errorMiddle, + onPressed: () => Navigator.pop(context, true), + child: Text(context.lang.delete), + ), + ), + ], + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/src/visual/components/selectable_thumbnail.comp.dart b/lib/src/visual/components/selectable_thumbnail.comp.dart index 572b017d..58c3c212 100644 --- a/lib/src/visual/components/selectable_thumbnail.comp.dart +++ b/lib/src/visual/components/selectable_thumbnail.comp.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/themes/light.dart'; class SelectableThumbnailComp extends StatelessWidget { const SelectableThumbnailComp({ @@ -19,7 +19,7 @@ class SelectableThumbnailComp extends StatelessWidget { duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, decoration: BoxDecoration( - color: isSelected ? context.color.primary : Colors.transparent, + color: isSelected ? primaryColor : Colors.transparent, boxShadow: const [ BoxShadow( color: Colors.black12, @@ -49,9 +49,7 @@ class SelectableThumbnailComp extends StatelessWidget { child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: isSelected - ? context.color.primary - : Colors.black38, + color: isSelected ? primaryColor : Colors.black38, shape: BoxShape.circle, border: Border.all( color: Theme.of(context).brightness == Brightness.dark diff --git a/lib/src/visual/elements/my_button.element.dart b/lib/src/visual/elements/my_button.element.dart index ee7ca730..c3038416 100644 --- a/lib/src/visual/elements/my_button.element.dart +++ b/lib/src/visual/elements/my_button.element.dart @@ -12,6 +12,7 @@ enum MyButtonVariant { secondaryDense, secondaryMiddle, error, + errorMiddle, } class MyButton extends StatefulWidget { @@ -33,7 +34,6 @@ class MyButton extends StatefulWidget { } class _MyButtonState extends State { - @override Widget build(BuildContext context) { final isEnabled = widget.onPressed != null || widget.onLongPress != null; @@ -189,6 +189,25 @@ class _MyButtonState extends State { fontWeight: FontWeight.bold, ), ); + case MyButtonVariant.errorMiddle: + buttonStyle = FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.errorContainer, + foregroundColor: Theme.of(context).colorScheme.onErrorContainer, + disabledBackgroundColor: disabledBgColor, + disabledForegroundColor: disabledFgColor, + minimumSize: const Size(0, 48), + padding: const EdgeInsets.symmetric( + horizontal: 24, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + elevation: 0, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ); } final childButton = widget.variant == MyButtonVariant.text diff --git a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart index dff1a207..2f018520 100644 --- a/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart +++ b/lib/src/visual/views/memories/components/memory_thumbnail.comp.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -91,13 +92,23 @@ class _MemoriesThumbnailCompState extends State { if (!hasThumbnail) { if (hasStored) { - media.createThumbnail(); + unawaited( + media.createThumbnail().then((_) { + if (mounted) { + _resolveImage(); + } + }), + ); } else { - MemoriesCloudService.downloadThumbnail(media).then((success) { - if (mounted && success) { - _resolveImage(); - } - }); + unawaited( + MemoriesCloudService.downloadFromCloud(media, isThumbnail: true).then( + (success) { + if (mounted && success) { + _resolveImage(); + } + }, + ), + ); } } @@ -117,6 +128,8 @@ class _MemoriesThumbnailCompState extends State { if (oldWidget.galleryItem.mediaService.mediaFile.mediaId != widget.galleryItem.mediaService.mediaFile.mediaId) { _imageStream?.removeListener(_listener); + _imageStream = null; + _imageProvider = null; _imageInfo = null; _retries = 0; _selectedImageFile = null; @@ -235,32 +248,45 @@ class _MemoriesThumbnailCompState extends State { ], ), ), - if (media.mediaFile.cloudState == CloudState.pending) - const Positioned( - top: 6, - right: 6, - child: Icon( - Icons.cloud_upload_outlined, - color: Colors.white70, - size: 16, - shadows: [ - Shadow(color: Colors.black54, blurRadius: 4), - ], - ), - ) - else if (media.mediaFile.cloudState == CloudState.uploaded) - const Positioned( - top: 6, - right: 6, - child: Icon( - Icons.cloud_done_outlined, - color: Colors.white, - size: 16, - shadows: [ - Shadow(color: Colors.black54, blurRadius: 4), - ], - ), - ), + Builder( + builder: (context) { + final hasStored = + media.storedPath.existsSync() && + media.storedPath.lengthSync() > 0; + final IconData iconData; + final Color color; + + switch (media.mediaFile.cloudState) { + case CloudState.none: + iconData = Icons.cloud_off_outlined; + color = Colors.white54; + case CloudState.pending: + iconData = Icons.cloud_upload_outlined; + color = Colors.white70; + case CloudState.uploaded: + if (hasStored) { + iconData = Icons.cloud_done_outlined; + color = Colors.white; + } else { + iconData = Icons.cloud_outlined; + color = Colors.white; + } + } + + return Positioned( + bottom: 6, + right: 6, + child: Icon( + iconData, + color: color, + size: 16, + shadows: const [ + Shadow(color: Colors.black54, blurRadius: 4), + ], + ), + ); + }, + ), ], ), ), diff --git a/lib/src/visual/views/memories/components/selection_menu.comp.dart b/lib/src/visual/views/memories/components/selection_menu.comp.dart new file mode 100644 index 00000000..a198f640 --- /dev/null +++ b/lib/src/visual/views/memories/components/selection_menu.comp.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class MemoriesSelectionMenuComp extends StatelessWidget { + const MemoriesSelectionMenuComp({ + required this.areAllSelected, + required this.onSelectAll, + required this.onExport, + required this.onFavorite, + required this.onDeleteCompletely, + required this.onDeleteLocally, + super.key, + }); + + final bool areAllSelected; + final VoidCallback onSelectAll; + final VoidCallback onExport; + final VoidCallback onFavorite; + final VoidCallback onDeleteCompletely; + final VoidCallback onDeleteLocally; + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + onSelected: (val) { + switch (val) { + case 'selectAll': + onSelectAll(); + case 'export': + onExport(); + case 'favorite': + onFavorite(); + case 'delete': + onDeleteCompletely(); + case 'deleteLocal': + onDeleteLocally(); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'selectAll', + child: Row( + children: [ + Icon( + areAllSelected ? Icons.deselect : Icons.select_all, + size: 20, + ), + const SizedBox(width: 12), + Text( + areAllSelected + ? context.lang.memoriesMenuDeselectAll + : context.lang.memoriesMenuSelectAll, + ), + ], + ), + ), + PopupMenuItem( + value: 'export', + child: Row( + children: [ + const Icon( + Icons.file_download_outlined, + size: 20, + ), + const SizedBox(width: 12), + Text(context.lang.memoriesMenuExport), + ], + ), + ), + PopupMenuItem( + value: 'favorite', + child: Row( + children: [ + const Icon( + Icons.favorite_border, + size: 20, + ), + const SizedBox(width: 12), + Text(context.lang.memoriesMenuFavorite), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + const Icon( + Icons.delete_forever_outlined, + size: 20, + color: Colors.redAccent, + ), + const SizedBox(width: 12), + Text( + context.lang.memoriesMenuDelete, + style: const TextStyle(color: Colors.redAccent), + ), + ], + ), + ), + PopupMenuItem( + value: 'deleteLocal', + child: Row( + children: [ + const Icon( + Icons.phonelink_erase_outlined, + size: 20, + ), + const SizedBox(width: 12), + Text(context.lang.memoriesMenuDeleteLocal), + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/memories/components/selection_toolbar.comp.dart b/lib/src/visual/views/memories/components/selection_toolbar.comp.dart deleted file mode 100644 index daf3b5a2..00000000 --- a/lib/src/visual/views/memories/components/selection_toolbar.comp.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class MemoriesSelectionToolbarComp extends StatelessWidget { - const MemoriesSelectionToolbarComp({ - required this.selectedCount, - required this.areAllSelected, - required this.areAllFav, - required this.onSelectAll, - required this.onExport, - required this.onFavorite, - required this.onDelete, - required this.onClear, - super.key, - }); - - final int selectedCount; - final bool areAllSelected; - final bool areAllFav; - final VoidCallback onSelectAll; - final VoidCallback onExport; - final VoidCallback onFavorite; - final VoidCallback onDelete; - final VoidCallback onClear; - - @override - Widget build(BuildContext context) { - return Positioned( - bottom: MediaQuery.paddingOf(context).bottom + 24, - left: 16, - right: 16, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: context.color.surface.withValues(alpha: 0.95), - borderRadius: BorderRadius.circular(20), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 16, - offset: Offset(0, 4), - ), - ], - border: Border.all( - color: context.color.primary.withValues(alpha: 0.2), - ), - ), - child: Row( - children: [ - Text( - '$selectedCount', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const Spacer(), - IconButton( - icon: Icon( - areAllSelected ? Icons.deselect : Icons.select_all, - size: 22, - ), - onPressed: onSelectAll, - tooltip: areAllSelected - ? context.lang.galleryDeselectAll - : context.lang.gallerySelectAll, - style: IconButton.styleFrom( - backgroundColor: context.color.primary.withValues( - alpha: 0.1, - ), - foregroundColor: context.color.primary, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.file_download_outlined, size: 22), - onPressed: onExport, - tooltip: context.lang.galleryExport, - style: IconButton.styleFrom( - backgroundColor: context.color.primary.withValues( - alpha: 0.1, - ), - foregroundColor: context.color.primary, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: Icon( - areAllFav ? Icons.favorite : Icons.favorite_border, - size: 22, - color: areAllFav ? Colors.redAccent : null, - ), - onPressed: onFavorite, - tooltip: areAllFav - ? context.lang.galleryUnfavorite - : context.lang.galleryFavorite, - style: IconButton.styleFrom( - backgroundColor: context.color.primary.withValues( - alpha: 0.1, - ), - foregroundColor: context.color.primary, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.delete_outline, size: 22), - onPressed: onDelete, - tooltip: context.lang.galleryDelete, - style: IconButton.styleFrom( - backgroundColor: Colors.redAccent.withValues( - alpha: 0.1, - ), - foregroundColor: Colors.redAccent, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: onClear, - tooltip: context.lang.galleryCancel, - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart b/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart index 1f786fbd..370e093a 100644 --- a/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart +++ b/lib/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/themes/light.dart'; class SynchronizedViewerActionsToolbarComp extends StatelessWidget { const SynchronizedViewerActionsToolbarComp({ @@ -27,94 +26,125 @@ class SynchronizedViewerActionsToolbarComp extends StatelessWidget { @override Widget build(BuildContext context) { - return Positioned( - bottom: MediaQuery.paddingOf(context).bottom + 24, - left: 0, - right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (showStoreButton) ...[ - IconButton( - icon: isImageSaving - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), + return Padding( + padding: const EdgeInsets.all(12), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.50), + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (showStoreButton) + _ToolbarAction( + icon: isImageSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const FaIcon( + FontAwesomeIcons.floppyDisk, + color: Colors.white, + size: 18, ), - ) - : const FaIcon( - FontAwesomeIcons.floppyDisk, - color: Colors.white, - size: 20, - ), - onPressed: isImageSaving ? null : onStore, - tooltip: 'Store media', - style: IconButton.styleFrom( - backgroundColor: Colors.black54, - padding: const EdgeInsets.all(12), + label: context.lang.galleryActionSave, + onTap: isImageSaving ? null : onStore, ), + _ToolbarAction( + icon: const FaIcon( + FontAwesomeIcons.download, + color: Colors.white, + size: 19, + ), + label: context.lang.galleryActionExport, + onTap: onExport, + ), + _ToolbarAction( + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? Colors.redAccent : Colors.white, + size: 22, + ), + label: isFavorite + ? context.lang.galleryActionUnfavorite + : context.lang.galleryActionFavorite, + onTap: onToggleFavorite, + ), + _ToolbarAction( + icon: const Icon( + Icons.delete, + color: Colors.white, + size: 22, + ), + label: context.lang.galleryActionDelete, + onTap: onDelete, + ), + _ToolbarAction( + icon: const FaIcon( + FontAwesomeIcons.shareNodes, + color: Colors.white, + size: 19, + ), + label: context.lang.galleryActionShare, + onTap: onShare, ), - const SizedBox(width: 16), ], - IconButton( - icon: const FaIcon( - FontAwesomeIcons.fileArrowDown, - color: Colors.white, - size: 21, - ), - onPressed: onExport, - tooltip: context.lang.galleryExport, - style: IconButton.styleFrom( - backgroundColor: Colors.black54, - padding: const EdgeInsets.all(12), - ), - ), - const SizedBox(width: 16), - IconButton( - icon: Icon( - isFavorite ? Icons.favorite : Icons.favorite_border, - color: isFavorite ? Colors.redAccent : Colors.white, - size: 24, - ), - onPressed: onToggleFavorite, - tooltip: 'Favorite', - style: IconButton.styleFrom( - backgroundColor: Colors.black54, - padding: const EdgeInsets.all(12), - ), - ), - const SizedBox(width: 16), - IconButton( - icon: const Icon( - Icons.delete_outline, - color: Colors.white, - size: 24, - ), - onPressed: onDelete, - tooltip: context.lang.galleryDelete, - style: IconButton.styleFrom( - backgroundColor: Colors.black54, - padding: const EdgeInsets.all(12), - ), - ), - const SizedBox(width: 16), - IconButton( - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, - color: primaryColor, - size: 22, - ), - onPressed: onShare, - tooltip: context.lang.shareImagedEditorSendImage, - style: IconButton.styleFrom( - backgroundColor: Colors.black54, - padding: const EdgeInsets.all(12), - ), - ), - ], + ), + ), + ); + } +} + +class _ToolbarAction extends StatelessWidget { + const _ToolbarAction({ + required this.icon, + required this.label, + required this.onTap, + }); + + final Widget icon; + final String label; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Opacity( + opacity: onTap == null ? 0.4 : 1.0, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 24, + child: Center(child: icon), + ), + const SizedBox(height: 6), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 10, + fontWeight: FontWeight.w600, + letterSpacing: 0.1, + ), + ), + ], + ), + ), + ), ), ); } diff --git a/lib/src/visual/views/memories/components/synchronized_viewer_item.comp.dart b/lib/src/visual/views/memories/components/synchronized_viewer_item.comp.dart new file mode 100644 index 00000000..083d3ca4 --- /dev/null +++ b/lib/src/visual/views/memories/components/synchronized_viewer_item.comp.dart @@ -0,0 +1,162 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/visual/helpers/video_player_file.helper.dart'; + +class SynchronizedViewerItemComp extends StatefulWidget { + const SynchronizedViewerItemComp({ + required this.item, + required this.currentlyViewedMediaIdNotifier, + required this.onZoomChanged, + super.key, + }); + + final MemoryItem item; + final ValueNotifier currentlyViewedMediaIdNotifier; + final ValueChanged onZoomChanged; + + @override + State createState() => + _SynchronizedViewerItemCompState(); +} + +class _SynchronizedViewerItemCompState + extends State { + bool _isDownloading = false; + + @override + void initState() { + super.initState(); + _checkAndDownload(); + } + + @override + void didUpdateWidget(covariant SynchronizedViewerItemComp oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.item.mediaService.mediaFile.mediaId != + widget.item.mediaService.mediaFile.mediaId) { + _checkAndDownload(); + } + } + + void _checkAndDownload() { + final media = widget.item.mediaService; + final hasStored = + media.storedPath.existsSync() && media.storedPath.lengthSync() > 0; + + if (!hasStored && + media.mediaFile.cloudState == CloudState.uploaded && + !_isDownloading) { + _isDownloading = true; + unawaited( + MemoriesCloudService.downloadFromCloud( + media, + isThumbnail: false, + ).then((success) async { + if (success && mounted) { + final fullPath = media.storedPath; + if (media.mediaFile.type == MediaType.image || + media.mediaFile.type == MediaType.gif) { + if (fullPath.existsSync()) { + try { + await precacheImage(FileImage(fullPath), context); + } catch (e) { + // Ignore precache errors + } + } + } + } + if (mounted) { + setState(() { + _isDownloading = false; + }); + } + }), + ); + } + } + + @override + Widget build(BuildContext context) { + final item = widget.item; + final itemMediaId = item.mediaService.mediaFile.mediaId; + + var filePath = item.mediaService.storedPath; + final hasStored = filePath.existsSync() && filePath.lengthSync() > 0; + if (!hasStored) { + filePath = item.mediaService.tempPath; + final hasTemp = filePath.existsSync() && filePath.lengthSync() > 0; + if (!hasTemp) { + filePath = item.mediaService.thumbnailPath; + } + } + + final isVideo = item.mediaService.mediaFile.type == MediaType.video; + final hasVideoFile = + isVideo && + (item.mediaService.storedPath.existsSync() && + item.mediaService.storedPath.lengthSync() > 0 || + item.mediaService.tempPath.existsSync() && + item.mediaService.tempPath.lengthSync() > 0); + + return Center( + child: ValueListenableBuilder( + valueListenable: widget.currentlyViewedMediaIdNotifier, + builder: (context, activeMediaId, childWidget) { + final isActiveTarget = activeMediaId == itemMediaId; + + if (isActiveTarget) { + return Hero( + tag: itemMediaId, + transitionOnUserGestures: true, + child: childWidget!, + ); + } + return childWidget!; + }, + child: !filePath.existsSync() || filePath.lengthSync() == 0 + ? item.mediaService.mediaFile.blurhash != null + ? BlurHash( + hash: item.mediaService.mediaFile.blurhash!, + optimizationMode: BlurHashOptimizationMode.approximation, + ) + : const Center( + child: Icon( + Icons.broken_image_outlined, + color: Colors.white38, + size: 64, + ), + ) + : hasVideoFile + ? VideoPlayerFileHelper(videoPath: filePath) + : PhotoView( + key: ValueKey(filePath.path), + imageProvider: FileImage(filePath), + initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 4.1, + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon( + Icons.broken_image_outlined, + color: Colors.white38, + size: 64, + ), + ); + }, + scaleStateChangedCallback: (state) { + final zoomed = state != PhotoViewScaleState.initial; + widget.onZoomChanged(zoomed); + }, + ), + ), + ); + } +} diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index b4071fc6..867e5516 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -7,11 +7,12 @@ import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/memories/memories.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart'; import 'package:twonly/src/visual/components/draggable_scrollbar.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart'; import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart'; -import 'package:twonly/src/visual/views/memories/components/selection_toolbar.comp.dart'; +import 'package:twonly/src/visual/views/memories/components/selection_menu.comp.dart'; import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart'; class MemoriesView extends StatefulWidget { @@ -270,57 +271,41 @@ class MemoriesViewState extends State } } - Future _batchDelete() async { + Future _batchDelete({bool? forceDeleteCompletely}) async { final count = _selectedMediaIds.length; final items = _service.currentState.galleryItems; final selectedList = _selectedMediaIds.toList(); - var hasCloudBackup = false; - for (final id in selectedList) { - final item = items - .where((e) => e.mediaService.mediaFile.mediaId == id) - .firstOrNull; - if (item != null && - item.mediaService.mediaFile.cloudState != CloudState.none) { - hasCloudBackup = true; - break; + var deleteCompletely = forceDeleteCompletely; + if (deleteCompletely == null) { + var hasCloudBackup = false; + for (final id in selectedList) { + final item = items + .where((e) => e.mediaService.mediaFile.mediaId == id) + .firstOrNull; + if (item != null && + item.mediaService.mediaFile.cloudState != CloudState.none) { + hasCloudBackup = true; + break; + } } - } - bool? deleteCompletely; - if (hasCloudBackup) { - deleteCompletely = await showDialog( + deleteCompletely = await showDeleteMemoriesDialog( context: context, - builder: (context) => AlertDialog( - title: Text(context.lang.deleteImageTitle), - content: Text(context.lang.deleteMemoriesBody(count)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.lang.galleryCancel), - ), - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Local Only'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Completely'), - ), - ], - ), + count: count, + hasCloudBackup: hasCloudBackup, ); - } else { + } else if (deleteCompletely) { final confirmed = await showAlertDialog( context, context.lang.deleteImageTitle, context.lang.deleteMemoriesBody(count), ); - if (confirmed) deleteCompletely = true; + if (!confirmed) return; } if (deleteCompletely == null) return; + final isCompletely = deleteCompletely; await _showProgressDialog( 'Deleting memories...', @@ -331,12 +316,14 @@ class MemoriesViewState extends State .where((e) => e.mediaService.mediaFile.mediaId == mediaId) .firstOrNull; if (item != null) { - if (deleteCompletely!) { + if (isCompletely) { item.mediaService.fullMediaRemoval(); await apiService.deleteMemory(mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); } else { - item.mediaService.storedPath.deleteSync(); + if (item.mediaService.storedPath.existsSync()) { + item.mediaService.storedPath.deleteSync(); + } } } setProgress((i + 1) / selectedList.length); @@ -515,9 +502,9 @@ class MemoriesViewState extends State vertical: 8, horizontal: 16, ), - child: const Text( - 'Cloud backup limit reached! Please upgrade your plan or free up space.', - style: TextStyle( + child: Text( + context.lang.memoriesBackupLimitReached, + style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), @@ -565,65 +552,128 @@ class MemoriesViewState extends State physics: const BouncingScrollPhysics(), slivers: [ SliverAppBar( - title: const Text( - 'Memories', - style: TextStyle(fontWeight: FontWeight.bold), - ), + leading: _selectionMode + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => + setState(_selectedMediaIds.clear), + ) + : null, + title: _selectionMode + ? Builder( + builder: (context) => Text( + context.lang.memoriesSelectedCount( + _selectedMediaIds.length, + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ) + : const Text( + 'Memories', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + pinned: _selectionMode, floating: true, snap: true, elevation: 0, backgroundColor: context.color.surface, - actions: [ - if (state.isLoading) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: Center( - child: Tooltip( - message: context.lang - .migrationOfMemories( - state.filesToMigrate, - ), - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - value: state.migrationProgress, - strokeWidth: 2.5, - valueColor: - AlwaysStoppedAnimation( - context.color.primary, + actions: _selectionMode + ? [ + IconButton( + icon: const Icon( + Icons.delete_outline, + ), + onPressed: _batchDelete, + ), + Builder( + builder: (context) { + final visibleCount = + _filterFavoritesOnly + ? state.galleryItems + .where( + (e) => e + .mediaService + .mediaFile + .isFavorite, + ) + .length + : state.galleryItems.length; + final areAllSelected = + visibleCount > 0 && + _selectedMediaIds.length >= + visibleCount; + + return MemoriesSelectionMenuComp( + areAllSelected: areAllSelected, + onSelectAll: _selectAll, + onExport: _batchExport, + onFavorite: _batchFavorite, + onDeleteCompletely: () => + _batchDelete( + forceDeleteCompletely: true, ), - backgroundColor: context - .color - .primary - .withValues(alpha: 0.2), + onDeleteLocally: () => + _batchDelete( + forceDeleteCompletely: + false, + ), + ); + }, + ), + ] + : [ + if (state.isLoading) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Center( + child: Tooltip( + message: context.lang + .migrationOfMemories( + state.filesToMigrate, + ), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + value: + state.migrationProgress, + strokeWidth: 2.5, + valueColor: + AlwaysStoppedAnimation( + context.color.primary, + ), + backgroundColor: context + .color + .primary + .withValues(alpha: 0.2), + ), + ), + ), ), ), + IconButton( + icon: Icon( + _filterFavoritesOnly + ? Icons.favorite + : Icons.favorite_border, + color: _filterFavoritesOnly + ? Colors.redAccent + : null, + ), + onPressed: () { + setState(() { + _filterFavoritesOnly = + !_filterFavoritesOnly; + }); + }, ), - ), - ), - IconButton( - icon: Icon( - _filterFavoritesOnly - ? Icons.favorite - : Icons.favorite_border, - color: _filterFavoritesOnly - ? Colors.redAccent - : null, - ), - onPressed: () { - setState(() { - _filterFavoritesOnly = - !_filterFavoritesOnly; - }); - }, - tooltip: _filterFavoritesOnly - ? 'Show all' - : 'Show favorites only', - ), - ], + ], ), MemoriesFlashbackBannerComp( lastYears: lastYears, @@ -703,47 +753,6 @@ class MemoriesViewState extends State ); }, ), - - if (_selectionMode) - Builder( - builder: (context) { - final items = _service.currentState.galleryItems; - var visibleCount = 0; - var favCount = 0; - - for (final item in items) { - final isFav = item.mediaService.mediaFile.isFavorite; - if (!_filterFavoritesOnly || isFav) { - visibleCount++; - } - if (_selectedMediaIds.contains( - item.mediaService.mediaFile.mediaId, - )) { - if (isFav) { - favCount++; - } - } - } - - final areAllSelected = - visibleCount > 0 && - _selectedMediaIds.length >= visibleCount; - final areAllFav = - _selectedMediaIds.isNotEmpty && - favCount == _selectedMediaIds.length; - - return MemoriesSelectionToolbarComp( - selectedCount: _selectedMediaIds.length, - areAllSelected: areAllSelected, - areAllFav: areAllFav, - onSelectAll: _selectAll, - onExport: _batchExport, - onFavorite: _batchFavorite, - onDelete: _batchDelete, - onClear: () => setState(_selectedMediaIds.clear), - ); - }, - ), ], ), ); diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index 97eeca52..c5ecbdbe 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -2,8 +2,6 @@ import 'dart:math'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_blurhash/flutter_blurhash.dart'; -import 'package:photo_view/photo_view.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -11,11 +9,11 @@ import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; -import 'package:twonly/src/visual/helpers/video_player_file.helper.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/views/memories/components/synchronized_viewer_actions_toolbar.comp.dart'; +import 'package:twonly/src/visual/views/memories/components/synchronized_viewer_item.comp.dart'; class SynchronizedImageViewerScreen extends StatefulWidget { const SynchronizedImageViewerScreen({ @@ -68,7 +66,7 @@ class _SynchronizedImageViewerScreenState if (item.mediaService.mediaFile.isFavorite) { _favoritedMediaIds.add(item.mediaService.mediaFile.mediaId); } - if (item.mediaService.storedPath.existsSync()) { + if (item.mediaService.mediaFile.stored) { _storedMediaIds.add(item.mediaService.mediaFile.mediaId); } } @@ -157,36 +155,46 @@ class _SynchronizedImageViewerScreenState } Future _deleteFile() async { - final confirmed = await showAlertDialog( - context, - context.lang.deleteImageTitle, - context.lang.deleteImageBody, + final item = widget.galleryItems[_currentIndex]; + final mediaId = item.mediaService.mediaFile.mediaId; + final hasCloudBackup = + item.mediaService.mediaFile.cloudState != CloudState.none; + + final deleteCompletely = await showDeleteMemoriesDialog( + context: context, + count: 1, + hasCloudBackup: hasCloudBackup, ); - if (!confirmed) return; + if (deleteCompletely == null) return; - widget.galleryItems[_currentIndex].mediaService.fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile( - widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId, - ); + if (deleteCompletely) { + item.mediaService.fullMediaRemoval(); + await apiService.deleteMemory(mediaId); + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); - widget.galleryItems.removeAt(_currentIndex); + widget.galleryItems.removeAt(_currentIndex); - if (widget.galleryItems.isEmpty) { - if (mounted) Navigator.pop(context, true); - return; + if (widget.galleryItems.isEmpty) { + if (mounted) Navigator.pop(context, true); + return; + } + + if (_currentIndex >= widget.galleryItems.length) { + _currentIndex = widget.galleryItems.length - 1; + } + + final newId = + widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId; + _currentlyViewedMediaIdNotifier.value = newId; + widget.activeMediaIdNotifier.value = newId; + + setState(() {}); + } else { + if (item.mediaService.storedPath.existsSync()) { + item.mediaService.storedPath.deleteSync(); + } } - - if (_currentIndex >= widget.galleryItems.length) { - _currentIndex = widget.galleryItems.length - 1; - } - - final newId = - widget.galleryItems[_currentIndex].mediaService.mediaFile.mediaId; - _currentlyViewedMediaIdNotifier.value = newId; - widget.activeMediaIdNotifier.value = newId; - - setState(() {}); } Future _exportFile() async { @@ -308,99 +316,50 @@ class _SynchronizedImageViewerScreenState widget.activeMediaIdNotifier.value = newMediaId; }, itemBuilder: (context, index) { - final item = widget.galleryItems[index]; - final itemMediaId = item.mediaService.mediaFile.mediaId; - - var filePath = item.mediaService.storedPath; - if (!filePath.existsSync()) { - filePath = item.mediaService.tempPath; - if (!filePath.existsSync()) { - filePath = item.mediaService.thumbnailPath; - } - } - - final isVideo = - item.mediaService.mediaFile.type == MediaType.video; - - return Center( - child: ValueListenableBuilder( - valueListenable: _currentlyViewedMediaIdNotifier, - builder: (context, activeMediaId, childWidget) { - // Dynamically resolve Hero tags to prevent layout tree duplicate assertions - final isActiveTarget = activeMediaId == itemMediaId; - - if (isActiveTarget) { - return Hero( - tag: itemMediaId, - transitionOnUserGestures: true, - child: childWidget!, - ); - } - return childWidget!; - }, - child: !filePath.existsSync() - ? item.mediaService.mediaFile.blurhash != null - ? BlurHash( - hash: item - .mediaService - .mediaFile - .blurhash!, - optimizationMode: - BlurHashOptimizationMode - .approximation, - ) - : const Center( - child: Icon( - Icons.broken_image_outlined, - color: Colors.white38, - size: 64, - ), - ) - : isVideo - ? VideoPlayerFileHelper(videoPath: filePath) - : PhotoView( - imageProvider: FileImage(filePath), - initialScale: - PhotoViewComputedScale.contained, - minScale: PhotoViewComputedScale.contained, - maxScale: - PhotoViewComputedScale.covered * 4.1, - backgroundDecoration: const BoxDecoration( - color: Colors.transparent, - ), - errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon( - Icons.broken_image_outlined, - color: Colors.white38, - size: 64, - ), - ); - }, - scaleStateChangedCallback: (state) { - final zoomed = - state != PhotoViewScaleState.initial; - if (_isZoomed != zoomed) { - setState(() { - _isZoomed = zoomed; - }); - } - }, - ), - ), + return SynchronizedViewerItemComp( + item: widget.galleryItems[index], + currentlyViewedMediaIdNotifier: + _currentlyViewedMediaIdNotifier, + onZoomChanged: (zoomed) { + if (_isZoomed != zoomed) { + setState(() { + _isZoomed = zoomed; + }); + } + }, ); }, ), - SynchronizedViewerActionsToolbarComp( - isFavorite: _favoritedMediaIds.contains(currentMediaId), - onShare: _shareMediaFile, - onExport: _exportFile, - onToggleFavorite: () => _toggleFavorite(currentMediaId), - onDelete: _deleteFile, - showStoreButton: !_storedMediaIds.contains(currentMediaId), - onStore: _storeMediaFile, - isImageSaving: _isSaving, + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 16, + left: 0, + right: 0, + child: AnimatedOpacity( + opacity: _isZoomed ? 0.0 : 1.0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + child: IgnorePointer( + ignoring: _isZoomed, + child: Center( + child: SynchronizedViewerActionsToolbarComp( + isFavorite: _favoritedMediaIds.contains( + currentMediaId, + ), + onShare: _shareMediaFile, + onExport: _exportFile, + onToggleFavorite: () => + _toggleFavorite(currentMediaId), + onDelete: _deleteFile, + showStoreButton: !_storedMediaIds.contains( + currentMediaId, + ), + onStore: _storeMediaFile, + isImageSaving: _isSaving, + ), + ), + ), + ), ), ], ), diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index a19c0652..b2c62163 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -5,10 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/model/json/backup.model.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' - as server; import 'package:twonly/src/services/backup.service.dart'; -import 'package:twonly/src/services/memories/memories_cloud.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart'; @@ -24,7 +21,6 @@ class BackupView extends StatefulWidget { class _BackupViewState extends State { bool _isLoading = false; CurrentBackupStatus? _backupStatus; - server.Response_MemoriesUsage? _memoriesUsage; StreamSubscription? _backupUpdateSub; @override @@ -45,11 +41,9 @@ class _BackupViewState extends State { Future _loadBackupStatus() async { setState(() => _isLoading = true); final status = await BackupService.getData(); - final memoriesUsage = await apiService.getMemoriesUsage(); if (!mounted) return; setState(() { _backupStatus = status; - _memoriesUsage = memoriesUsage; _isLoading = false; }); } @@ -188,58 +182,6 @@ class _BackupViewState extends State { ), ]), ), - const SizedBox(height: 24), - const Center( - child: Text( - 'Memories Backup', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - const SizedBox(height: 8), - Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - children: _buildTableRows([ - ( - 'Usage', - _memoriesUsage != null - ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' - : '-', - ), - ( - 'Files Backed Up', - _memoriesUsage != null - ? '${_memoriesUsage!.count}' - : '-', - ), - ]), - ), - StreamBuilder( - initialData: memoriesCloudService.currentProgress, - stream: memoriesCloudService.progressStream, - builder: (context, snapshot) { - final progress = snapshot.data; - if (progress == null || progress.totalPending == 0) { - return const SizedBox.shrink(); - } - final percent = (progress.currentUploaded / progress.totalPending) + - (progress.currentUploadProgress / progress.totalPending); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Text( - 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 6), - LinearProgressIndicator( - value: percent.clamp(0.0, 1.0), - ), - ], - ); - }, - ), ], ), @@ -250,7 +192,7 @@ class _BackupViewState extends State { variant: MyButtonVariant.primaryMiddle, onPressed: () => context.navPush(const PasswordLessRecoverySetup()), - child: const Text('Setup Passwordless Recovery'), + child: Text(context.lang.passwordlessRecoveryEnableBtn), ), ), ], diff --git a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart index 73308b3b..6731d122 100644 --- a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart @@ -1,6 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' + as server; +import 'package:twonly/src/providers/purchases.provider.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; class ManageStorageView extends StatefulWidget { @@ -12,6 +20,7 @@ class ManageStorageView extends StatefulWidget { class _ManageStorageViewState extends State { Map _stats = {}; + server.Response_MemoriesUsage? _memoriesUsage; @override void initState() { @@ -21,15 +30,20 @@ class _ManageStorageViewState extends State { Future _loadStats() async { final stats = await twonlyDB.mediaFilesDao.getStorageStats(); + final memoriesUsage = await apiService.getMemoriesUsage(); if (mounted) { setState(() { _stats = stats; + _memoriesUsage = memoriesUsage; }); } } @override Widget build(BuildContext context) { + final currentPlan = context.watch().plan; + final isFreePlan = currentPlan == SubscriptionPlan.Free; + final totalBytes = _stats.entries .where((e) => e.key != MediaType.audio) .fold(0, (a, b) => a + b.value); @@ -44,8 +58,148 @@ class _ManageStorageViewState extends State { body: ListView( padding: const EdgeInsets.all(16), children: [ + if (isFreePlan) ...[ + Card( + elevation: 0, + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of( + context, + ).colorScheme.outlineVariant.withValues(alpha: 0.5), + ), + ), + child: InkWell( + onTap: () => context.push(Routes.settingsSubscription), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.cloud_off_outlined, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.settingsStorageNoCloudBackupTitle, + style: Theme.of(context).textTheme.titleSmall + ?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + context.lang.settingsStorageNoCloudBackupCard, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 24), + ] else ...[ + Text( + 'Memories Backup', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + _memoriesUsage != null + ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' + : '-', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Container( + height: 24, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: LayoutBuilder( + builder: (context, constraints) { + if (_memoriesUsage == null || + _memoriesUsage!.maxBytes == 0) { + return const SizedBox.shrink(); + } + + final maxWidth = constraints.maxWidth; + final current = _memoriesUsage!.currentBytes.toDouble(); + final max = _memoriesUsage!.maxBytes.toDouble(); + final usageWidth = + ((current / max).clamp(0.0, 1.0)) * maxWidth; + + return Row( + children: [ + if (usageWidth > 0) + Container( + width: usageWidth, + color: Colors.blue, + ), + ], + ); + }, + ), + ), + ), + StreamBuilder( + initialData: memoriesCloudService.currentProgress, + stream: memoriesCloudService.progressStream, + builder: (context, snapshot) { + final progress = snapshot.data; + if (progress == null || progress.totalPending == 0) { + return const SizedBox.shrink(); + } + final percent = + (progress.currentUploaded / progress.totalPending) + + (progress.currentUploadProgress / progress.totalPending); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text( + 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: percent.clamp(0.0, 1.0), + ), + ], + ); + }, + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 24), + ], Text( - context.lang.settingsStorageUsed, + isFreePlan + ? context.lang.settingsStorageUsed + : context.lang.settingsStorageLocal, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), From 1891f1ee185b617895fc4c044b3fe697ae45ef6a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Jul 2026 23:06:06 +0200 Subject: [PATCH 19/29] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1e6a3b7b..7eb06e49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.4.1+158 +version: 0.4.1+159 environment: sdk: ^3.11.0 From 644a2c1e3ad5c3a68e5efd612e730d55f1c58a8c Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Jul 2026 13:24:46 +0200 Subject: [PATCH 20/29] fix performance issue --- .../services/memories/memories.service.dart | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/src/services/memories/memories.service.dart b/lib/src/services/memories/memories.service.dart index 868ebcde..035d49e8 100644 --- a/lib/src/services/memories/memories.service.dart +++ b/lib/src/services/memories/memories.service.dart @@ -4,6 +4,7 @@ import 'dart:collection'; import 'package:blurhash_dart/blurhash_dart.dart' as bh; import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; +import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; @@ -259,7 +260,12 @@ class MemoriesService { // Phase 2: Background — hash, crop analysis, size calculation. // Each DB write here fires the stream subscription above, keeping // the gallery state fresh without a separate notification step. - await _backgroundProcessPendingFiles(pendingFiles); + _dbSubscription?.pause(); + try { + await _backgroundProcessPendingFiles(pendingFiles); + } finally { + _dbSubscription?.resume(); + } } catch (e) { Log.error('Error in background migration queue: $e'); } @@ -291,9 +297,8 @@ class MemoriesService { : mediaService.originalPath; if (imageFile.existsSync()) { final bytes = await imageFile.readAsBytes(); - final image = img.decodeImage(bytes); - if (image != null) { - final blurhash = bh.BlurHash.encode(image).hash; + final blurhash = await compute(_calculateBlurhash, bytes); + if (blurhash != null) { await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, MediaFilesCompanion(blurhash: Value(blurhash)), @@ -384,3 +389,13 @@ class MemoriesService { _stateController.close(); } } + +String? _calculateBlurhash(Uint8List bytes) { + try { + final image = img.decodeImage(bytes); + if (image != null) { + return bh.BlurHash.encode(image).hash; + } + } catch (_) {} + return null; +} From 54538a6c31f48edeb1236f84e5d0a2ab00165cb3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Jul 2026 13:25:15 +0200 Subject: [PATCH 21/29] remove orphaned media files --- .../services/api/mediafiles/upload.api.dart | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index 205e04ab..c318350f 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -222,6 +222,19 @@ Future finishStartedPreprocessing() async { Log.info('Ignoring media files as it is a draft'); continue; } + + final messages = await twonlyDB.messagesDao.getMessagesByMediaId( + mediaFile.mediaId, + ); + if (messages.isEmpty) { + Log.info( + 'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.', + ); + MediaFileService(mediaFile).fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + continue; + } + try { final service = MediaFileService(mediaFile); if (!service.originalPath.existsSync() && @@ -492,20 +505,51 @@ Future _startBackgroundMediaUploadInternal( } if (!mediaService.encryptedPath.existsSync()) { + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Encrypted file not found. Starting encryption.', + ); await _encryptMediaFiles(mediaService); if (!mediaService.encryptedPath.existsSync()) { + Log.warn( + 'Media ${mediaService.mediaFile.mediaId}: Encryption failed. Encrypted file still missing.', + ); return; } + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Encryption completed successfully.', + ); + } else { + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Encrypted file already exists.', + ); } if (!mediaService.uploadRequestPath.existsSync()) { + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Upload request file not found. Creating it.', + ); await _createUploadRequest(mediaService); + if (!mediaService.uploadRequestPath.existsSync()) { + Log.warn( + 'Media ${mediaService.mediaFile.mediaId}: Upload request file creation returned empty (e.g. no messages).', + ); + } + } else { + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Upload request file already exists.', + ); } if (mediaService.uploadRequestPath.existsSync()) { + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Transitioning uploadState from ${mediaService.mediaFile.uploadState} to uploading.', + ); await mediaService.setUploadState(UploadState.uploading); // at this point the original file is not used any more, so it can be deleted if (mediaService.originalPath.existsSync()) { + Log.info( + 'Media ${mediaService.mediaFile.mediaId}: Deleting original file as it is now encrypted.', + ); mediaService.originalPath.deleteSync(); } } @@ -553,6 +597,9 @@ Future _createUploadRequest(MediaFileService media) async { if (messages.isEmpty) { // There where no user selected who should receive the image, so waiting with this step... + Log.info( + 'Media ${media.mediaFile.mediaId}: No recipient messages found, waiting to create upload request', + ); return; } From fb9acc690ad5ac68e6a62a8ca9bdc459818192f6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Jul 2026 14:18:09 +0200 Subject: [PATCH 22/29] add promo for enabling e2ee backup --- .../generated/app_localizations.dart | 12 ++ .../generated/app_localizations_de.dart | 6 + .../generated/app_localizations_en.dart | 6 + lib/src/localization/translations | 2 +- lib/src/model/json/userdata.model.dart | 10 +- lib/src/model/json/userdata.model.g.dart | 7 +- .../memories/memories_cloud.service.dart | 2 +- .../components/cloud_backup_promo.comp.dart | 108 ++++++++++ .../visual/views/memories/memories.view.dart | 3 + .../data_and_storage/manage_storage.view.dart | 184 +++++++++++------- 10 files changed, 264 insertions(+), 76 deletions(-) create mode 100644 lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 348d0259..e7ec988e 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -4171,6 +4171,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Share'** String get galleryActionShare; + + /// No description provided for @settingsStorageHidePromo. + /// + /// In en, this message translates to: + /// **'Hide'** + String get settingsStorageHidePromo; + + /// No description provided for @memoriesBackupTitle. + /// + /// In en, this message translates to: + /// **'Memories Backup'** + String get memoriesBackupTitle; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 88402311..68185e49 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2415,4 +2415,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get galleryActionShare => 'Teilen'; + + @override + String get settingsStorageHidePromo => 'Ausblenden'; + + @override + String get memoriesBackupTitle => 'Memories Backup'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 1c4ab096..9e93e569 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2394,4 +2394,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get galleryActionShare => 'Share'; + + @override + String get settingsStorageHidePromo => 'Hide'; + + @override + String get memoriesBackupTitle => 'Memories Backup'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 9d05713a..f72c6178 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 9d05713a1f69e7e4de742c414df7df6c4c7925a7 +Subproject commit f72c61787096a44e2252058c08e4c14a0a8e5cff diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 1a05106c..8ee4b648 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -103,6 +103,9 @@ class UserData { @JsonKey(defaultValue: false) bool screenLockEnabled = false; + @JsonKey(defaultValue: false) + bool isCloudBackupEnabled = false; + // > User Discovery Configurations @JsonKey(defaultValue: false) @@ -139,6 +142,9 @@ class UserData { @JsonKey(defaultValue: true) bool hideChangeLog = true; + @JsonKey(defaultValue: false) + bool hideMemoriesBackupPromo = false; + @JsonKey(defaultValue: true) bool updateFCMToken = true; @@ -150,8 +156,8 @@ class UserData { @Deprecated('Use the secure storage in rust') TwonlySafeBackup? twonlySafeBackup; - @JsonKey(defaultValue: true) - bool isBackupEnabled = true; + @JsonKey(defaultValue: false) + bool isBackupEnabled = false; PasswordLessRecovery? passwordLessRecovery; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index b560d6d4..08825f09 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -68,6 +68,7 @@ UserData _$UserDataFromJson(Map json) => ..allowErrorTrackingViaSentry = json['allowErrorTrackingViaSentry'] as bool? ?? false ..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false + ..isCloudBackupEnabled = json['isCloudBackupEnabled'] as bool? ?? false ..isUserDiscoveryEnabled = json['isUserDiscoveryEnabled'] as bool? ?? false ..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4 @@ -88,6 +89,8 @@ UserData _$UserDataFromJson(Map json) => ?.map((e) => (e as num).toInt()) .toList() ..hideChangeLog = json['hideChangeLog'] as bool? ?? true + ..hideMemoriesBackupPromo = + json['hideMemoriesBackupPromo'] as bool? ?? false ..updateFCMToken = json['updateFCMToken'] as bool? ?? true ..canUseLoginTokenForAuth = json['canUseLoginTokenForAuth'] as bool? ?? true @@ -96,7 +99,7 @@ UserData _$UserDataFromJson(Map json) => : TwonlySafeBackup.fromJson( json['twonlySafeBackup'] as Map, ) - ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? true + ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false ..passwordLessRecovery = json['passwordLessRecovery'] == null ? null : PasswordLessRecovery.fromJson( @@ -153,6 +156,7 @@ Map _$UserDataToJson(UserData instance) => { ?.toIso8601String(), 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'screenLockEnabled': instance.screenLockEnabled, + 'isCloudBackupEnabled': instance.isCloudBackupEnabled, 'isUserDiscoveryEnabled': instance.isUserDiscoveryEnabled, 'requiredSendImages': instance.requiredSendImages, 'userDiscoveryThreshold': instance.userDiscoveryThreshold, @@ -165,6 +169,7 @@ Map _$UserDataToJson(UserData instance) => { 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, 'hideChangeLog': instance.hideChangeLog, + 'hideMemoriesBackupPromo': instance.hideMemoriesBackupPromo, 'updateFCMToken': instance.updateFCMToken, 'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth, 'twonlySafeBackup': instance.twonlySafeBackup, diff --git a/lib/src/services/memories/memories_cloud.service.dart b/lib/src/services/memories/memories_cloud.service.dart index fc6c3e21..4cab3a2a 100644 --- a/lib/src/services/memories/memories_cloud.service.dart +++ b/lib/src/services/memories/memories_cloud.service.dart @@ -101,7 +101,7 @@ class MemoriesCloudService { } Future checkUploads() async { - if (_isProcessing || !userService.currentUser.isBackupEnabled) return; + if (_isProcessing || !userService.currentUser.isCloudBackupEnabled) return; try { final memories = await twonlyDB.mediaFilesDao.getMemoriesToBackup(); diff --git a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart new file mode 100644 index 00000000..47db82ea --- /dev/null +++ b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; + +class MemoriesCloudBackupPromoComp extends StatelessWidget { + const MemoriesCloudBackupPromoComp({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + final user = userService.currentUser; + if (user.isCloudBackupEnabled || user.hideMemoriesBackupPromo) { + return const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); + } + + return SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + context.color.primaryContainer.withValues(alpha: 0.15), + context.color.primaryContainer.withValues(alpha: 0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + border: Border.all( + color: context.color.primary.withValues(alpha: 0.1), + width: 1.5, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.memoriesBackupTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.color.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + context.lang.settingsStorageNoCloudBackupCard, + style: TextStyle( + fontSize: 13, + color: context.color.onSurfaceVariant, + height: 1.3, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + MyButton( + variant: MyButtonVariant.primaryDense, + onPressed: () async { + await UserService.update( + (u) => u.isCloudBackupEnabled = true, + ); + unawaited(memoriesCloudService.checkUploads()); + }, + child: Text(context.lang.enable), + ), + const SizedBox(width: 8), + MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: () async { + await UserService.update( + (u) => u.hideMemoriesBackupPromo = true, + ); + }, + child: Text( + context.lang.settingsStorageHidePromo, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 867e5516..4664cb63 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; @@ -10,6 +11,7 @@ import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart'; import 'package:twonly/src/visual/components/draggable_scrollbar.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/views/memories/components/cloud_backup_promo.comp.dart'; import 'package:twonly/src/visual/views/memories/components/flashback_banner.comp.dart'; import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.comp.dart'; import 'package:twonly/src/visual/views/memories/components/selection_menu.comp.dart'; @@ -680,6 +682,7 @@ class MemoriesViewState extends State onOpenFlashback: (items, idx) => _openViewer(items, idx, isFlashback: true), ), + const MemoriesCloudBackupPromoComp(), for (final month in months) ...[ SliverPadding( padding: const EdgeInsets.fromLTRB( diff --git a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart index 6731d122..9eb13ff8 100644 --- a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -9,7 +10,9 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; import 'package:twonly/src/services/subscription.service.dart'; +import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; class ManageStorageView extends StatefulWidget { const ManageStorageView({super.key}); @@ -117,84 +120,123 @@ class _ManageStorageViewState extends State { const SizedBox(height: 24), ] else ...[ Text( - 'Memories Backup', + context.lang.memoriesBackupTitle, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), - Text( - _memoriesUsage != null - ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' - : '-', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - Container( - height: 24, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(12), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: LayoutBuilder( - builder: (context, constraints) { - if (_memoriesUsage == null || - _memoriesUsage!.maxBytes == 0) { - return const SizedBox.shrink(); - } - - final maxWidth = constraints.maxWidth; - final current = _memoriesUsage!.currentBytes.toDouble(); - final max = _memoriesUsage!.maxBytes.toDouble(); - final usageWidth = - ((current / max).clamp(0.0, 1.0)) * maxWidth; - - return Row( + if (!userService.currentUser.isCloudBackupEnabled) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (usageWidth > 0) - Container( - width: usageWidth, - color: Colors.blue, - ), + Text( + context.lang.settingsStorageNoCloudBackupCard, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), ], - ); - }, + ), + ), + const SizedBox(width: 16), + MyButton( + variant: MyButtonVariant.primaryMiddle, + onPressed: () async { + await UserService.update( + (u) => u.isCloudBackupEnabled = true, + ); + setState(() {}); + unawaited(memoriesCloudService.checkUploads()); + }, + child: Text(context.lang.enable), + ), + ], + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 24), + ] else ...[ + Text( + _memoriesUsage != null + ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' + : '-', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, ), ), - ), - StreamBuilder( - initialData: memoriesCloudService.currentProgress, - stream: memoriesCloudService.progressStream, - builder: (context, snapshot) { - final progress = snapshot.data; - if (progress == null || progress.totalPending == 0) { - return const SizedBox.shrink(); - } - final percent = - (progress.currentUploaded / progress.totalPending) + - (progress.currentUploadProgress / progress.totalPending); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Text( - 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 6), - LinearProgressIndicator( - value: percent.clamp(0.0, 1.0), - ), - ], - ); - }, - ), - const SizedBox(height: 24), - const Divider(), - const SizedBox(height: 24), + const SizedBox(height: 24), + Container( + height: 24, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: LayoutBuilder( + builder: (context, constraints) { + if (_memoriesUsage == null || + _memoriesUsage!.maxBytes == 0) { + return const SizedBox.shrink(); + } + + final maxWidth = constraints.maxWidth; + final current = _memoriesUsage!.currentBytes.toDouble(); + final max = _memoriesUsage!.maxBytes.toDouble(); + final usageWidth = + ((current / max).clamp(0.0, 1.0)) * maxWidth; + + return Row( + children: [ + if (usageWidth > 0) + Container( + width: usageWidth, + color: Colors.blue, + ), + ], + ); + }, + ), + ), + ), + StreamBuilder( + initialData: memoriesCloudService.currentProgress, + stream: memoriesCloudService.progressStream, + builder: (context, snapshot) { + final progress = snapshot.data; + if (progress == null || progress.totalPending == 0) { + return const SizedBox.shrink(); + } + final percent = + (progress.currentUploaded / progress.totalPending) + + (progress.currentUploadProgress / progress.totalPending); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text( + 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: percent.clamp(0.0, 1.0), + ), + ], + ); + }, + ), + const SizedBox(height: 24), + const Divider(), + const SizedBox(height: 24), + ], ], Text( isFreePlan From 9f8cb6f735a176ce5d0ef28ab0c2ae662e0643f4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Jul 2026 14:40:27 +0200 Subject: [PATCH 23/29] news for new blog post --- lib/locator.dart | 5 +- lib/main.dart | 7 + lib/src/constants/routes.keys.dart | 1 + .../generated/app_localizations.dart | 6 + .../generated/app_localizations_de.dart | 3 + .../generated/app_localizations_en.dart | 3 + lib/src/localization/translations | 2 +- lib/src/providers/routing.provider.dart | 5 + lib/src/services/news.service.dart | 257 ++++++++++++++++++ .../visual/views/chats/chat_list.view.dart | 4 +- .../chat_list_components/news_btn.comp.dart | 29 ++ .../visual/views/settings/help/news.view.dart | 120 ++++++++ test/services/news_service_test.dart | 178 ++++++++++++ 13 files changed, 616 insertions(+), 4 deletions(-) create mode 100644 lib/src/services/news.service.dart create mode 100644 lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart create mode 100644 lib/src/visual/views/settings/help/news.view.dart create mode 100644 test/services/news_service_test.dart diff --git a/lib/locator.dart b/lib/locator.dart index 8c6806bc..4fa2f7b4 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,6 +1,7 @@ import 'package:get_it/get_it.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/api.service.dart'; +import 'package:twonly/src/services/news.service.dart'; import 'package:twonly/src/services/user.service.dart'; final GetIt locator = GetIt.instance; @@ -9,9 +10,11 @@ void setupLocator() { locator ..registerLazySingleton(UserService.new) ..registerLazySingleton(ApiService.new) - ..registerLazySingleton(TwonlyDB.new); + ..registerLazySingleton(TwonlyDB.new) + ..registerLazySingleton(NewsService.new); } UserService get userService => locator(); ApiService get apiService => locator(); TwonlyDB get twonlyDB => locator(); +NewsService get newsService => locator(); diff --git a/lib/main.dart b/lib/main.dart index b1114ec1..c375f023 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -175,6 +175,13 @@ Future postStartupTasks() async { // 2. Service initializations unawaited(finishStartedPreprocessing()); unawaited(createPushAvatars()); + unawaited(newsService.init().then((_) { + final lastDownload = newsService.lastDownloadedAt; + if (lastDownload == null || + DateTime.now().difference(lastDownload) >= const Duration(days: 7)) { + newsService.fetchFeed(); + } + })); unawaited(UserDiscoveryService.verifyInitializationOnStartup()); diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index 43fc9cb0..9df0ea84 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -48,6 +48,7 @@ class Routes { static const String settingsHelpFaqVerifyBadge = '/settings/help/faq/verifybadge'; static const String settingsHelpContactUs = '/settings/help/contact_us'; + static const String settingsHelpNews = '/settings/help/news'; static const String settingsHelpDiagnostics = '/settings/help/diagnostics'; static const String settingsHelpUserStudy = '/settings/help/user_study'; static const String settingsHelpUserStudyQuestionnaire = diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index e7ec988e..73b4bfce 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -4183,6 +4183,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Memories Backup'** String get memoriesBackupTitle; + + /// No description provided for @settingsHelpNews. + /// + /// In en, this message translates to: + /// **'News'** + String get settingsHelpNews; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 68185e49..37df22b6 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2421,4 +2421,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get memoriesBackupTitle => 'Memories Backup'; + + @override + String get settingsHelpNews => 'Neuigkeiten'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 9e93e569..c87ff548 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2400,4 +2400,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get memoriesBackupTitle => 'Memories Backup'; + + @override + String get settingsHelpNews => 'News'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index f72c6178..bb353aa5 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit f72c61787096a44e2252058c08e4c14a0a8e5cff +Subproject commit bb353aa572623d20578f470d2c683ead79669ed8 diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 44228345..2b239d00 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -37,6 +37,7 @@ import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq/verification_badge_faq.view.dart'; import 'package:twonly/src/visual/views/settings/help/help.view.dart'; +import 'package:twonly/src/visual/views/settings/help/news.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart'; @@ -259,6 +260,10 @@ final routerProvider = GoRouter( path: 'contact_us', builder: (context, state) => const ContactUsView(), ), + GoRoute( + path: 'news', + builder: (context, state) => const NewsView(), + ), GoRoute( path: 'diagnostics', builder: (context, state) => const DiagnosticsView(), diff --git a/lib/src/services/news.service.dart b/lib/src/services/news.service.dart new file mode 100644 index 00000000..116c9638 --- /dev/null +++ b/lib/src/services/news.service.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/log.dart'; + +class BlogEntry { + BlogEntry({ + required this.title, + required this.link, + required this.guid, + required this.description, + required this.imageUrl, + this.pubDate, + }); + + factory BlogEntry.fromJson(Map json) => BlogEntry( + title: (json['title'] as String?) ?? '', + link: (json['link'] as String?) ?? '', + guid: (json['guid'] as String?) ?? '', + description: (json['description'] as String?) ?? '', + imageUrl: (json['imageUrl'] as String?) ?? '', + pubDate: json['pubDate'] != null ? DateTime.tryParse(json['pubDate'] as String) : null, + ); + + final String title; + final String link; + final String guid; + final String description; + final String imageUrl; + final DateTime? pubDate; + + Map toJson() => { + 'title': title, + 'link': link, + 'guid': guid, + 'description': description, + 'imageUrl': imageUrl, + 'pubDate': pubDate?.toIso8601String(), + }; + + @override + String toString() { + return 'BlogEntry(title: $title, link: $link, guid: $guid, description: $description, imageUrl: $imageUrl, pubDate: $pubDate)'; + } +} + +class NewsService { + List entries = []; + Set openedGuids = {}; + DateTime? lastDownloadedAt; + bool hasLoadedBefore = false; + + final ValueNotifier unreadCountNotifier = ValueNotifier(0); + + File get _cacheFile => File(join(AppEnvironment.supportDir, 'news_feed.json')); + + Future init() async { + try { + await loadCache(); + } catch (e) { + Log.error('Failed to load news cache: $e'); + } + } + + Future loadCache() async { + final file = _cacheFile; + if (!file.existsSync()) { + return; + } + final content = await file.readAsString(); + final json = jsonDecode(content) as Map; + + hasLoadedBefore = json['hasLoadedBefore'] as bool? ?? false; + + final lastDownloadedStr = json['lastDownloadedAt'] as String?; + if (lastDownloadedStr != null) { + lastDownloadedAt = DateTime.tryParse(lastDownloadedStr); + } + + final openedList = json['openedGuids'] as List? ?? []; + openedGuids = openedList.map((e) => e.toString()).toSet(); + + final entriesList = json['entries'] as List? ?? []; + entries = entriesList + .map((e) => BlogEntry.fromJson(e as Map)) + .toList(); + + _updateUnreadCount(); + } + + Future saveCache() async { + final file = _cacheFile; + final json = { + 'hasLoadedBefore': hasLoadedBefore, + 'lastDownloadedAt': lastDownloadedAt?.toIso8601String(), + 'openedGuids': openedGuids.toList(), + 'entries': entries.map((e) => e.toJson()).toList(), + }; + await file.writeAsString(jsonEncode(json)); + } + + void _updateUnreadCount() { + var count = 0; + for (final entry in entries) { + if (!openedGuids.contains(entry.guid)) { + count++; + } + } + unreadCountNotifier.value = count; + } + + Future markAllAsRead() async { + for (final entry in entries) { + openedGuids.add(entry.guid); + } + _updateUnreadCount(); + await saveCache(); + } + + Future fetchFeed({http.Client? client, bool force = false}) async { + try { + final lang = ui.PlatformDispatcher.instance.locale.languageCode; + final url = lang == 'de' + ? 'https://twonly.eu/de/blog/rss.xml' + : 'https://twonly.eu/en/blog/rss.xml'; + + final response = client != null + ? await client.get(Uri.parse(url)) + : await http.get(Uri.parse(url)); + if (response.statusCode != 200) { + Log.error('Failed to download RSS feed: code ${response.statusCode}'); + return; + } + + final xml = utf8.decode(response.bodyBytes); + final newEntries = _parseRss(xml); + + if (!hasLoadedBefore) { + // First run: mark all fetched as already opened + for (final entry in newEntries) { + openedGuids.add(entry.guid); + } + hasLoadedBefore = true; + } + + entries = newEntries; + lastDownloadedAt = DateTime.now(); + + _updateUnreadCount(); + await saveCache(); + Log.info('Successfully fetched ${entries.length} news entries. Unread count: ${unreadCountNotifier.value}'); + } catch (e) { + Log.error('Failed to fetch news feed: $e'); + } + } + + List _parseRss(String xml) { + final entries = []; + final itemRegex = RegExp(r'([\s\S]*?)<\/item>'); + final matches = itemRegex.allMatches(xml); + + for (final match in matches) { + final itemContent = match.group(1) ?? ''; + + final titleMatch = RegExp(r'([\s\S]*?)<\/title>').firstMatch(itemContent); + final title = _stripCdata(titleMatch?.group(1) ?? ''); + + final linkMatch = RegExp(r'<link>([\s\S]*?)<\/link>').firstMatch(itemContent); + final link = _stripCdata(linkMatch?.group(1) ?? ''); + + final guidMatch = RegExp(r'<guid[^>]*?>([\s\S]*?)<\/guid>').firstMatch(itemContent); + final guid = _stripCdata(guidMatch?.group(1) ?? link); + + final descMatch = RegExp(r'<description>([\s\S]*?)<\/description>').firstMatch(itemContent); + final descriptionRaw = _stripCdata(descMatch?.group(1) ?? ''); + final description = descriptionRaw.replaceAll(RegExp('<[^>]*>'), ''); + + var imageUrl = ''; + final enclosureMatch = RegExp(r'<enclosure\s+[^>]*?url="([^"]+)"').firstMatch(itemContent); + if (enclosureMatch != null) { + imageUrl = enclosureMatch.group(1) ?? ''; + } + if (imageUrl.isEmpty) { + final mediaMatch = RegExp(r'<media:content\s+[^>]*?url="([^"]+)"').firstMatch(itemContent); + if (mediaMatch != null) { + imageUrl = mediaMatch.group(1) ?? ''; + } + } + + final pubDateMatch = RegExp(r'<pubDate>([\s\S]*?)<\/pubDate>').firstMatch(itemContent); + DateTime? pubDate; + if (pubDateMatch != null) { + final pubDateStr = pubDateMatch.group(1) ?? ''; + pubDate = DateTime.tryParse(pubDateStr); + pubDate ??= _parseRfc2822(pubDateStr); + } + + entries.add(BlogEntry( + title: title, + link: link, + guid: guid, + description: description, + imageUrl: imageUrl, + pubDate: pubDate, + )); + } + + return entries; + } + + String _stripCdata(String input) { + var s = input.trim(); + if (s.startsWith('<![CDATA[')) { + s = s.substring(9); + } + if (s.endsWith(']]>')) { + s = s.substring(0, s.length - 3); + } + return s.trim(); + } + + DateTime? _parseRfc2822(String dateString) { + try { + var cleaned = dateString.trim(); + if (cleaned.contains(',')) { + cleaned = cleaned.split(',')[1].trim(); + } + final parts = cleaned.split(RegExp(r'\s+')); + if (parts.length < 4) return null; + + final day = int.tryParse(parts[0]) ?? 1; + final monthStr = parts[1].toLowerCase(); + final year = int.tryParse(parts[2]) ?? DateTime.now().year; + + final timeParts = parts[3].split(':'); + final hour = timeParts.isNotEmpty ? (int.tryParse(timeParts[0]) ?? 0) : 0; + final minute = timeParts.length > 1 ? (int.tryParse(timeParts[1]) ?? 0) : 0; + final second = timeParts.length > 2 ? (int.tryParse(timeParts[2]) ?? 0) : 0; + + const months = { + 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, + 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12 + }; + final monthStringPart = monthStr.length > 3 ? monthStr.substring(0, 3) : monthStr; + final month = months[monthStringPart] ?? 1; + + return DateTime.utc(year, month, day, hour, minute, second); + } catch (e) { + Log.warn('Failed to parse date: $e'); + return null; + } + } +} diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 58de7f31..cc8078fa 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -17,8 +17,8 @@ import 'package:twonly/src/visual/components/connection_status.comp.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart'; -import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; +import 'package:twonly/src/visual/views/chats/chat_list_components/news_btn.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart'; @@ -184,7 +184,7 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie ], ), actions: [ - const FeedbackIconButtonComp(), + const NewsIconButtonComp(), ValueListenableBuilder<int>( valueListenable: _badgeCount, builder: (context, badgeCount, child) { diff --git a/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart b/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart new file mode 100644 index 00000000..a914bac3 --- /dev/null +++ b/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/notification_badge.comp.dart'; + +class NewsIconButtonComp extends StatelessWidget { + const NewsIconButtonComp({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder<int>( + valueListenable: newsService.unreadCountNotifier, + builder: (context, count, child) { + return NotificationBadgeComp( + count: count.toString(), + child: IconButton( + onPressed: () => context.push(Routes.settingsHelpNews), + color: Colors.grey, + tooltip: context.lang.settingsHelpNews, + icon: const FaIcon(FontAwesomeIcons.newspaper, size: 19), + ), + ); + }, + ); + } +} diff --git a/lib/src/visual/views/settings/help/news.view.dart b/lib/src/visual/views/settings/help/news.view.dart new file mode 100644 index 00000000..f65c6f7a --- /dev/null +++ b/lib/src/visual/views/settings/help/news.view.dart @@ -0,0 +1,120 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class NewsView extends StatefulWidget { + const NewsView({super.key}); + + @override + State<NewsView> createState() => _NewsViewState(); +} + +class _NewsViewState extends State<NewsView> { + @override + void initState() { + super.initState(); + // Mark all as read when entering the page + newsService.markAllAsRead(); + } + + @override + Widget build(BuildContext context) { + final entries = newsService.entries; + + return Scaffold( + appBar: AppBar( + title: Text(context.lang.settingsHelpNews), + ), + body: entries.isEmpty + ? Center( + child: Text( + 'No news articles found.', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.grey, + ), + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: entries.length, + itemBuilder: (context, index) { + final entry = entries[index]; + return Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 2, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: InkWell( + onTap: () => launchUrl( + Uri.parse(entry.link), + mode: LaunchMode.externalApplication, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (entry.imageUrl.isNotEmpty) + CachedNetworkImage( + imageUrl: entry.imageUrl, + height: 180, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + height: 180, + color: Colors.grey.withValues(alpha: 0.1), + child: const Center( + child: CircularProgressIndicator(), + ), + ), + errorWidget: (context, url, error) => Container( + height: 180, + color: Colors.grey.withValues(alpha: 0.1), + child: const Icon(Icons.broken_image, size: 50, color: Colors.grey), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (entry.pubDate != null) ...[ + Text( + DateFormat.yMMMMd( + Localizations.localeOf(context).toString(), + ).format(entry.pubDate!), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + ], + Text( + entry.title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + entry.description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurface.withValues(alpha: 0.8), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/test/services/news_service_test.dart b/test/services/news_service_test.dart new file mode 100644 index 00000000..041e0e6b --- /dev/null +++ b/test/services/news_service_test.dart @@ -0,0 +1,178 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:path/path.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/news.service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late Directory tempDir; + late NewsService newsServiceInstance; + + setUp(() async { + tempDir = Directory.systemTemp.createTempSync('news_service_test'); + AppEnvironment.initTesting( + customCacheDir: join(tempDir.path, 'cache'), + customSupportDir: join(tempDir.path, 'support'), + ); + Directory(AppEnvironment.supportDir).createSync(recursive: true); + Directory(AppEnvironment.cacheDir).createSync(recursive: true); + + await locator.reset(); + locator.registerSingleton<NewsService>(NewsService()); + newsServiceInstance = newsService; + }); + + tearDown(() async { + if (tempDir.existsSync()) { + tempDir.deleteSync(recursive: true); + } + }); + + const rssXml = ''' +<?xml version="1.0" encoding="UTF-8" ?> +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"> +<channel> + <title>twonly Blog + https://twonly.eu/en/blog/ + Latest news, privacy insights, and development updates from twonly. + en + Mon, 20 Jul 2026 12:23:32 GMT + + + <![CDATA[Finding Friends Without Phone Numbers]]> + https://twonly.eu/en/blog/2026-mutual-friends.html + https://twonly.eu/en/blog/2026-mutual-friends.html + Sun, 03 May 2026 00:00:00 GMT + + + + + + +'''; + + const rssXmlWithTwoItems = ''' + + + + twonly Blog + https://twonly.eu/en/blog/ + Latest news, privacy insights, and development updates from twonly. + en + Mon, 20 Jul 2026 12:23:32 GMT + + + <![CDATA[Finding Friends Without Phone Numbers]]> + https://twonly.eu/en/blog/2026-mutual-friends.html + https://twonly.eu/en/blog/2026-mutual-friends.html + Sun, 03 May 2026 00:00:00 GMT + + + + + + + <![CDATA[New Update Available]]> + https://twonly.eu/en/blog/new-update.html + https://twonly.eu/en/blog/new-update.html + Mon, 20 Jul 2026 12:00:00 GMT + + new version of twonly has been released.]]> + + + +'''; + + test('Initial fetch feed marks all articles as read', () async { + final mockClient = MockClient((request) async { + return http.Response.bytes(utf8.encode(rssXml), 200); + }); + + await newsServiceInstance.init(); + expect(newsServiceInstance.hasLoadedBefore, isFalse); + expect(newsServiceInstance.unreadCountNotifier.value, 0); + + await newsServiceInstance.fetchFeed(client: mockClient); + + expect(newsServiceInstance.hasLoadedBefore, isTrue); + expect(newsServiceInstance.entries.length, 1); + expect(newsServiceInstance.unreadCountNotifier.value, 0); + expect( + newsServiceInstance.openedGuids.contains( + 'https://twonly.eu/en/blog/2026-mutual-friends.html', + ), + isTrue, + ); + }); + + test('Subsequent fetch feed detects new articles as unread', () async { + final mockClient1 = MockClient((request) async { + return http.Response.bytes(utf8.encode(rssXml), 200); + }); + + await newsServiceInstance.init(); + await newsServiceInstance.fetchFeed(client: mockClient1); + + expect(newsServiceInstance.unreadCountNotifier.value, 0); + + final mockClient2 = MockClient((request) async { + return http.Response.bytes(utf8.encode(rssXmlWithTwoItems), 200); + }); + + await newsServiceInstance.fetchFeed(client: mockClient2); + + expect(newsServiceInstance.entries.length, 2); + expect(newsServiceInstance.unreadCountNotifier.value, 1); + expect( + newsServiceInstance.entries[1].description, + 'A brand new version of twonly has been released.', + ); + }); + + test('markAllAsRead clears unread count', () async { + final mockClient1 = MockClient((request) async { + return http.Response.bytes(utf8.encode(rssXml), 200); + }); + + await newsServiceInstance.init(); + await newsServiceInstance.fetchFeed(client: mockClient1); + + final mockClient2 = MockClient((request) async { + return http.Response.bytes(utf8.encode(rssXmlWithTwoItems), 200); + }); + + await newsServiceInstance.fetchFeed(client: mockClient2); + expect(newsServiceInstance.unreadCountNotifier.value, 1); + + await newsServiceInstance.markAllAsRead(); + expect(newsServiceInstance.unreadCountNotifier.value, 0); + }); + + test('Cache loads correctly and restores state', () async { + final mockClient = MockClient((request) async { + return http.Response.bytes(utf8.encode(rssXml), 200); + }); + + await newsServiceInstance.init(); + await newsServiceInstance.fetchFeed(client: mockClient); + + final newServiceInstance = NewsService(); + await newServiceInstance.init(); + + expect(newServiceInstance.hasLoadedBefore, isTrue); + expect(newServiceInstance.entries.length, 1); + expect(newServiceInstance.unreadCountNotifier.value, 0); + expect( + newServiceInstance.openedGuids.contains( + 'https://twonly.eu/en/blog/2026-mutual-friends.html', + ), + isTrue, + ); + }); +} From 7e3caec42ea2269e0f074c7b1731467dcc8a72a0 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Jul 2026 19:25:45 +0200 Subject: [PATCH 24/29] show news icon --- .../generated/app_localizations.dart | 6 +- .../generated/app_localizations_de.dart | 2 +- .../generated/app_localizations_en.dart | 2 +- lib/src/localization/translations | 2 +- lib/src/model/json/userdata.model.dart | 2 +- lib/src/model/json/userdata.model.g.dart | 4 +- lib/src/services/news.service.dart | 119 ++++++++++++------ .../feedback_btn.comp.dart | 28 ----- .../chat_list_components/news_btn.comp.dart | 33 +++-- .../views/settings/appearance.view.dart | 12 +- .../visual/views/settings/help/news.view.dart | 53 +++++--- 11 files changed, 152 insertions(+), 111 deletions(-) delete mode 100644 lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 73b4bfce..1630b743 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -806,11 +806,11 @@ abstract class AppLocalizations { /// **'Feedback submitted successfully!'** String get contactUsSuccess; - /// No description provided for @contactUsShortcut. + /// No description provided for @hideNewsIcon. /// /// In en, this message translates to: - /// **'Hide Feedback Icon'** - String get contactUsShortcut; + /// **'Hide News Icon'** + String get hideNewsIcon; /// No description provided for @settingsHelpTerms. /// diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 37df22b6..cee06bc0 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -386,7 +386,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contactUsSuccess => 'Feedback erfolgreich übermittelt!'; @override - String get contactUsShortcut => 'Feedback-Symbol ausblenden'; + String get hideNewsIcon => 'Neuigkeiten-Symbol ausblenden'; @override String get settingsHelpTerms => 'Nutzungsbedingungen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index c87ff548..fa36c5c3 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -382,7 +382,7 @@ class AppLocalizationsEn extends AppLocalizations { String get contactUsSuccess => 'Feedback submitted successfully!'; @override - String get contactUsShortcut => 'Hide Feedback Icon'; + String get hideNewsIcon => 'Hide News Icon'; @override String get settingsHelpTerms => 'Terms of Service'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index bb353aa5..1727cd6d 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit bb353aa572623d20578f470d2c683ead79669ed8 +Subproject commit 1727cd6d4f2c56f976883497c182b1456033b614 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 8ee4b648..8c3e3fe0 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -72,7 +72,7 @@ class UserData { bool videoStabilizationEnabled = true; @JsonKey(defaultValue: true) - bool showFeedbackShortcut = true; + bool showNewsShortcut = true; @JsonKey(defaultValue: false) bool showShowImagePreviewWhenSending = false; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 08825f09..cf2b4b9f 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -41,7 +41,7 @@ UserData _$UserDataFromJson(Map json) => json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false ..videoStabilizationEnabled = json['videoStabilizationEnabled'] as bool? ?? true - ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true + ..showNewsShortcut = json['showNewsShortcut'] as bool? ?? true ..showShowImagePreviewWhenSending = json['showShowImagePreviewWhenSending'] as bool? ?? false ..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true @@ -142,7 +142,7 @@ Map _$UserDataToJson(UserData instance) => { 'automaticallyMarkEqualMediaFilesAsOpened': instance.automaticallyMarkEqualMediaFilesAsOpened, 'videoStabilizationEnabled': instance.videoStabilizationEnabled, - 'showFeedbackShortcut': instance.showFeedbackShortcut, + 'showNewsShortcut': instance.showNewsShortcut, 'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending, 'startWithCameraOpen': instance.startWithCameraOpen, 'preSelectedEmojies': instance.preSelectedEmojies, diff --git a/lib/src/services/news.service.dart b/lib/src/services/news.service.dart index 116c9638..63c00b93 100644 --- a/lib/src/services/news.service.dart +++ b/lib/src/services/news.service.dart @@ -18,13 +18,15 @@ class BlogEntry { }); factory BlogEntry.fromJson(Map json) => BlogEntry( - title: (json['title'] as String?) ?? '', - link: (json['link'] as String?) ?? '', - guid: (json['guid'] as String?) ?? '', - description: (json['description'] as String?) ?? '', - imageUrl: (json['imageUrl'] as String?) ?? '', - pubDate: json['pubDate'] != null ? DateTime.tryParse(json['pubDate'] as String) : null, - ); + title: (json['title'] as String?) ?? '', + link: (json['link'] as String?) ?? '', + guid: (json['guid'] as String?) ?? '', + description: (json['description'] as String?) ?? '', + imageUrl: (json['imageUrl'] as String?) ?? '', + pubDate: json['pubDate'] != null + ? DateTime.tryParse(json['pubDate'] as String) + : null, + ); final String title; final String link; @@ -34,13 +36,13 @@ class BlogEntry { final DateTime? pubDate; Map toJson() => { - 'title': title, - 'link': link, - 'guid': guid, - 'description': description, - 'imageUrl': imageUrl, - 'pubDate': pubDate?.toIso8601String(), - }; + 'title': title, + 'link': link, + 'guid': guid, + 'description': description, + 'imageUrl': imageUrl, + 'pubDate': pubDate?.toIso8601String(), + }; @override String toString() { @@ -56,7 +58,8 @@ class NewsService { final ValueNotifier unreadCountNotifier = ValueNotifier(0); - File get _cacheFile => File(join(AppEnvironment.supportDir, 'news_feed.json')); + File get _cacheFile => + File(join(AppEnvironment.supportDir, 'news_feed.json')); Future init() async { try { @@ -152,7 +155,9 @@ class NewsService { _updateUnreadCount(); await saveCache(); - Log.info('Successfully fetched ${entries.length} news entries. Unread count: ${unreadCountNotifier.value}'); + Log.info( + 'Successfully fetched ${entries.length} news entries. Unread count: ${unreadCountNotifier.value}', + ); } catch (e) { Log.error('Failed to fetch news feed: $e'); } @@ -166,32 +171,54 @@ class NewsService { for (final match in matches) { final itemContent = match.group(1) ?? ''; - final titleMatch = RegExp(r'([\s\S]*?)<\/title>').firstMatch(itemContent); + final titleMatch = RegExp( + r'<title>([\s\S]*?)<\/title>', + ).firstMatch(itemContent); final title = _stripCdata(titleMatch?.group(1) ?? ''); - final linkMatch = RegExp(r'<link>([\s\S]*?)<\/link>').firstMatch(itemContent); + final linkMatch = RegExp( + r'<link>([\s\S]*?)<\/link>', + ).firstMatch(itemContent); final link = _stripCdata(linkMatch?.group(1) ?? ''); - final guidMatch = RegExp(r'<guid[^>]*?>([\s\S]*?)<\/guid>').firstMatch(itemContent); + final guidMatch = RegExp( + r'<guid[^>]*?>([\s\S]*?)<\/guid>', + ).firstMatch(itemContent); final guid = _stripCdata(guidMatch?.group(1) ?? link); - final descMatch = RegExp(r'<description>([\s\S]*?)<\/description>').firstMatch(itemContent); + final descMatch = RegExp( + r'<description>([\s\S]*?)<\/description>', + ).firstMatch(itemContent); final descriptionRaw = _stripCdata(descMatch?.group(1) ?? ''); - final description = descriptionRaw.replaceAll(RegExp('<[^>]*>'), ''); + final description = descriptionRaw + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") + .replaceAll(RegExp('<[^>]*>'), '') + .trim(); var imageUrl = ''; - final enclosureMatch = RegExp(r'<enclosure\s+[^>]*?url="([^"]+)"').firstMatch(itemContent); + final enclosureMatch = RegExp( + r'<enclosure\s+[^>]*?url="([^"]+)"', + ).firstMatch(itemContent); if (enclosureMatch != null) { imageUrl = enclosureMatch.group(1) ?? ''; } if (imageUrl.isEmpty) { - final mediaMatch = RegExp(r'<media:content\s+[^>]*?url="([^"]+)"').firstMatch(itemContent); + final mediaMatch = RegExp( + r'<media:content\s+[^>]*?url="([^"]+)"', + ).firstMatch(itemContent); if (mediaMatch != null) { imageUrl = mediaMatch.group(1) ?? ''; } } - final pubDateMatch = RegExp(r'<pubDate>([\s\S]*?)<\/pubDate>').firstMatch(itemContent); + final pubDateMatch = RegExp( + r'<pubDate>([\s\S]*?)<\/pubDate>', + ).firstMatch(itemContent); DateTime? pubDate; if (pubDateMatch != null) { final pubDateStr = pubDateMatch.group(1) ?? ''; @@ -199,14 +226,16 @@ class NewsService { pubDate ??= _parseRfc2822(pubDateStr); } - entries.add(BlogEntry( - title: title, - link: link, - guid: guid, - description: description, - imageUrl: imageUrl, - pubDate: pubDate, - )); + entries.add( + BlogEntry( + title: title, + link: link, + guid: guid, + description: description, + imageUrl: imageUrl, + pubDate: pubDate, + ), + ); } return entries; @@ -238,14 +267,30 @@ class NewsService { final timeParts = parts[3].split(':'); final hour = timeParts.isNotEmpty ? (int.tryParse(timeParts[0]) ?? 0) : 0; - final minute = timeParts.length > 1 ? (int.tryParse(timeParts[1]) ?? 0) : 0; - final second = timeParts.length > 2 ? (int.tryParse(timeParts[2]) ?? 0) : 0; + final minute = timeParts.length > 1 + ? (int.tryParse(timeParts[1]) ?? 0) + : 0; + final second = timeParts.length > 2 + ? (int.tryParse(timeParts[2]) ?? 0) + : 0; const months = { - 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, - 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12 + 'jan': 1, + 'feb': 2, + 'mar': 3, + 'apr': 4, + 'may': 5, + 'jun': 6, + 'jul': 7, + 'aug': 8, + 'sep': 9, + 'oct': 10, + 'nov': 11, + 'dec': 12, }; - final monthStringPart = monthStr.length > 3 ? monthStr.substring(0, 3) : monthStr; + final monthStringPart = monthStr.length > 3 + ? monthStr.substring(0, 3) + : monthStr; final month = months[monthStringPart] ?? 1; return DateTime.utc(year, month, day, hour, minute, second); diff --git a/lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart b/lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart deleted file mode 100644 index 498af280..00000000 --- a/lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:go_router/go_router.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class FeedbackIconButtonComp extends StatelessWidget { - const FeedbackIconButtonComp({super.key}); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: userService.onUserUpdated, - builder: (context, asyncSnapshot) { - if (!userService.currentUser.showFeedbackShortcut) { - return const SizedBox.shrink(); - } - return IconButton( - onPressed: () => context.push(Routes.settingsHelpContactUs), - color: Colors.grey, - tooltip: context.lang.feedbackTooltip, - icon: const FaIcon(FontAwesomeIcons.commentDots, size: 19), - ); - }, - ); - } -} diff --git a/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart b/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart index a914bac3..05ba9752 100644 --- a/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart @@ -5,23 +5,34 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; class NewsIconButtonComp extends StatelessWidget { const NewsIconButtonComp({super.key}); @override Widget build(BuildContext context) { - return ValueListenableBuilder<int>( - valueListenable: newsService.unreadCountNotifier, - builder: (context, count, child) { - return NotificationBadgeComp( - count: count.toString(), - child: IconButton( - onPressed: () => context.push(Routes.settingsHelpNews), - color: Colors.grey, - tooltip: context.lang.settingsHelpNews, - icon: const FaIcon(FontAwesomeIcons.newspaper, size: 19), - ), + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + if (!userService.currentUser.showNewsShortcut) { + return const SizedBox.shrink(); + } + return ValueListenableBuilder<int>( + valueListenable: newsService.unreadCountNotifier, + builder: (context, count, child) { + return NotificationBadgeComp( + count: count.toString(), + backgroundColor: primaryColor, + textColor: Colors.black87, + child: IconButton( + onPressed: () => context.push(Routes.settingsHelpNews), + color: Colors.grey, + tooltip: context.lang.settingsHelpNews, + icon: const FaIcon(FontAwesomeIcons.bullhorn, size: 19), + ), + ); + }, ); }, ); diff --git a/lib/src/visual/views/settings/appearance.view.dart b/lib/src/visual/views/settings/appearance.view.dart index e01051be..4caa5a0a 100644 --- a/lib/src/visual/views/settings/appearance.view.dart +++ b/lib/src/visual/views/settings/appearance.view.dart @@ -72,9 +72,9 @@ class _AppearanceViewState extends State<AppearanceView> { } } - Future<void> toggleShowFeedbackIcon() async { + Future<void> toggleShowNewsIcon() async { await UserService.update((u) { - u.showFeedbackShortcut = !u.showFeedbackShortcut; + u.showNewsShortcut = !u.showNewsShortcut; }); } @@ -113,11 +113,11 @@ class _AppearanceViewState extends State<AppearanceView> { }, ), ListTile( - title: Text(context.lang.contactUsShortcut), - onTap: toggleShowFeedbackIcon, + title: Text(context.lang.hideNewsIcon), + onTap: toggleShowNewsIcon, trailing: Switch.adaptive( - value: !userService.currentUser.showFeedbackShortcut, - onChanged: (a) => toggleShowFeedbackIcon(), + value: !userService.currentUser.showNewsShortcut, + onChanged: (a) => toggleShowNewsIcon(), ), ), ListTile( diff --git a/lib/src/visual/views/settings/help/news.view.dart b/lib/src/visual/views/settings/help/news.view.dart index f65c6f7a..fcc59eb8 100644 --- a/lib/src/visual/views/settings/help/news.view.dart +++ b/lib/src/visual/views/settings/help/news.view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart'; import 'package:url_launcher/url_launcher.dart'; class NewsView extends StatefulWidget { @@ -33,8 +34,8 @@ class _NewsViewState extends State<NewsView> { child: Text( 'No news articles found.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.grey, - ), + color: Colors.grey, + ), ), ) : ListView.builder( @@ -42,17 +43,23 @@ class _NewsViewState extends State<NewsView> { itemCount: entries.length, itemBuilder: (context, index) { final entry = entries[index]; - return Card( - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + final isDark = isDarkMode(context); + + return ReactiveTapFeedback( + onTap: () => launchUrl( + Uri.parse(entry.link), + mode: LaunchMode.externalApplication, ), - elevation: 2, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: InkWell( - onTap: () => launchUrl( - Uri.parse(entry.link), - mode: LaunchMode.externalApplication, + child: Card( + color: isDark ? Colors.grey[800] : Colors.grey[200], + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -65,14 +72,15 @@ class _NewsViewState extends State<NewsView> { placeholder: (context, url) => Container( height: 180, color: Colors.grey.withValues(alpha: 0.1), - child: const Center( - child: CircularProgressIndicator(), - ), ), errorWidget: (context, url, error) => Container( height: 180, color: Colors.grey.withValues(alpha: 0.1), - child: const Icon(Icons.broken_image, size: 50, color: Colors.grey), + child: const Icon( + Icons.broken_image, + size: 50, + color: Colors.grey, + ), ), ), Padding( @@ -85,7 +93,8 @@ class _NewsViewState extends State<NewsView> { DateFormat.yMMMMd( Localizations.localeOf(context).toString(), ).format(entry.pubDate!), - style: Theme.of(context).textTheme.bodySmall?.copyWith( + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( color: Colors.grey, ), ), @@ -93,7 +102,8 @@ class _NewsViewState extends State<NewsView> { ], Text( entry.title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( + style: Theme.of(context).textTheme.titleLarge + ?.copyWith( fontWeight: FontWeight.bold, ), ), @@ -102,8 +112,11 @@ class _NewsViewState extends State<NewsView> { entry.description, maxLines: 3, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: context.color.onSurface.withValues(alpha: 0.8), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: context.color.onSurface.withValues( + alpha: 0.8, + ), ), ), ], From c651ee8c323e279cfe0329db6abab6b43453cede Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Mon, 20 Jul 2026 19:34:57 +0200 Subject: [PATCH 25/29] precache images in the memories viewer --- .../memories/synchronized_viewer.view.dart | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index c5ecbdbe..45e3897e 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -42,6 +42,7 @@ class _SynchronizedImageViewerScreenState final Set<String> _favoritedMediaIds = {}; bool _isSaving = false; final Set<String> _storedMediaIds = {}; + final Set<String> _precachedMediaIds = {}; late int _currentIndex; bool _isZoomed = false; @@ -55,11 +56,15 @@ class _SynchronizedImageViewerScreenState final initialId = widget.galleryItems[widget.initialIndex].mediaService.mediaFile.mediaId; _currentlyViewedMediaIdNotifier = ValueNotifier(initialId); + _precachedMediaIds.add(initialId); _horizontalPager = PageController(initialPage: widget.initialIndex); _verticalPager = PageController(initialPage: 1); WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated); + if (mounted) { + _verticalPager.addListener(_onVerticalScrollUpdated); + _precacheNeighbours(_currentIndex); + } }); for (final item in widget.galleryItems) { @@ -314,6 +319,8 @@ class _SynchronizedImageViewerScreenState .mediaId; _currentlyViewedMediaIdNotifier.value = newMediaId; widget.activeMediaIdNotifier.value = newMediaId; + _precachedMediaIds.add(newMediaId); + _precacheNeighbours(idx); }, itemBuilder: (context, index) { return SynchronizedViewerItemComp( @@ -369,4 +376,40 @@ class _SynchronizedImageViewerScreenState ), ); } + + void _precacheNeighbours(int index) { + if (!mounted) return; + + final indicesToPrecache = [index - 5, index + 5]; + + for (final idx in indicesToPrecache) { + if (idx >= 0 && idx < widget.galleryItems.length) { + final item = widget.galleryItems[idx]; + if (item.mediaService.mediaFile.type == MediaType.video) { + continue; + } + + final mediaId = item.mediaService.mediaFile.mediaId; + if (_precachedMediaIds.contains(mediaId)) { + continue; + } + + final filePath = + item.mediaService.storedPath.existsSync() && + item.mediaService.storedPath.lengthSync() > 0 + ? item.mediaService.storedPath + : item.mediaService.tempPath.existsSync() && + item.mediaService.tempPath.lengthSync() > 0 + ? item.mediaService.tempPath + : item.mediaService.thumbnailPath; + + if (filePath.existsSync() && filePath.lengthSync() > 0) { + _precachedMediaIds.add(mediaId); + precacheImage(FileImage(filePath), context).onError((e, s) { + _precachedMediaIds.remove(mediaId); + }); + } + } + } + } } From ee82a95861329657c539aa135293404a232c426e Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Mon, 20 Jul 2026 20:11:31 +0200 Subject: [PATCH 26/29] show storage content --- .../generated/app_localizations.dart | 48 ++ .../generated/app_localizations_de.dart | 24 + .../generated/app_localizations_en.dart | 24 + lib/src/localization/translations | 2 +- .../components/selection_menu.comp.dart | 26 +- .../memories/synchronized_viewer.view.dart | 4 +- .../data_and_storage/manage_storage.view.dart | 9 + .../storage_contents.view.dart | 409 ++++++++++++++++++ 8 files changed, 532 insertions(+), 14 deletions(-) create mode 100644 lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 1630b743..df6aaf34 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -4189,6 +4189,54 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'News'** String get settingsHelpNews; + + /// No description provided for @settingsStorageContents. + /// + /// In en, this message translates to: + /// **'Storage contents'** + String get settingsStorageContents; + + /// No description provided for @settingsStorageSortStorage. + /// + /// In en, this message translates to: + /// **'Occupied storage'** + String get settingsStorageSortStorage; + + /// No description provided for @settingsStorageSortNewest. + /// + /// In en, this message translates to: + /// **'Newest'** + String get settingsStorageSortNewest; + + /// No description provided for @settingsStorageSortOldest. + /// + /// In en, this message translates to: + /// **'Oldest'** + String get settingsStorageSortOldest; + + /// No description provided for @settingsStorageNoContents. + /// + /// In en, this message translates to: + /// **'No storage contents found.'** + String get settingsStorageNoContents; + + /// No description provided for @settingsStorageCloudOnly. + /// + /// In en, this message translates to: + /// **'Cloud only'** + String get settingsStorageCloudOnly; + + /// No description provided for @settingsStorageLocalAndCloud. + /// + /// In en, this message translates to: + /// **'Local and cloud'** + String get settingsStorageLocalAndCloud; + + /// No description provided for @settingsStorageLocalOnly. + /// + /// In en, this message translates to: + /// **'Local only'** + String get settingsStorageLocalOnly; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index cee06bc0..f8a094f0 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2424,4 +2424,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsHelpNews => 'Neuigkeiten'; + + @override + String get settingsStorageContents => 'Speicherinhalte'; + + @override + String get settingsStorageSortStorage => 'Belegter Speicher'; + + @override + String get settingsStorageSortNewest => 'Neueste'; + + @override + String get settingsStorageSortOldest => 'Älteste'; + + @override + String get settingsStorageNoContents => 'Keine Speicherinhalte gefunden.'; + + @override + String get settingsStorageCloudOnly => 'Nur Cloud'; + + @override + String get settingsStorageLocalAndCloud => 'Lokal & Cloud'; + + @override + String get settingsStorageLocalOnly => 'Nur lokal'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index fa36c5c3..1b98deda 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2403,4 +2403,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsHelpNews => 'News'; + + @override + String get settingsStorageContents => 'Storage contents'; + + @override + String get settingsStorageSortStorage => 'Occupied storage'; + + @override + String get settingsStorageSortNewest => 'Newest'; + + @override + String get settingsStorageSortOldest => 'Oldest'; + + @override + String get settingsStorageNoContents => 'No storage contents found.'; + + @override + String get settingsStorageCloudOnly => 'Cloud only'; + + @override + String get settingsStorageLocalAndCloud => 'Local and cloud'; + + @override + String get settingsStorageLocalOnly => 'Local only'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 1727cd6d..bcfb2b02 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 1727cd6d4f2c56f976883497c182b1456033b614 +Subproject commit bcfb2b02c59d1e3afe66b310296cb0f03eb75a72 diff --git a/lib/src/visual/views/memories/components/selection_menu.comp.dart b/lib/src/visual/views/memories/components/selection_menu.comp.dart index a198f640..4433f90c 100644 --- a/lib/src/visual/views/memories/components/selection_menu.comp.dart +++ b/lib/src/visual/views/memories/components/selection_menu.comp.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/misc.dart'; class MemoriesSelectionMenuComp extends StatelessWidget { @@ -97,19 +98,20 @@ class MemoriesSelectionMenuComp extends StatelessWidget { ], ), ), - PopupMenuItem( - value: 'deleteLocal', - child: Row( - children: [ - const Icon( - Icons.phonelink_erase_outlined, - size: 20, - ), - const SizedBox(width: 12), - Text(context.lang.memoriesMenuDeleteLocal), - ], + if (userService.currentUser.isCloudBackupEnabled) + PopupMenuItem( + value: 'deleteLocal', + child: Row( + children: [ + const Icon( + Icons.phonelink_erase_outlined, + size: 20, + ), + const SizedBox(width: 12), + Text(context.lang.memoriesMenuDeleteLocal), + ], + ), ), - ), ], ); } diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index 45e3897e..b1a80246 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -71,7 +71,9 @@ class _SynchronizedImageViewerScreenState if (item.mediaService.mediaFile.isFavorite) { _favoritedMediaIds.add(item.mediaService.mediaFile.mediaId); } - if (item.mediaService.mediaFile.stored) { + if (item.mediaService.mediaFile.stored || + (item.mediaService.storedPath.existsSync() && + item.mediaService.storedPath.lengthSync() > 0)) { _storedMediaIds.add(item.mediaService.mediaFile.mediaId); } } diff --git a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart index 9eb13ff8..c3ffc121 100644 --- a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart'; class ManageStorageView extends StatefulWidget { const ManageStorageView({super.key}); @@ -300,6 +301,14 @@ class _ManageStorageViewState extends State<ManageStorageView> { size: formatBytes(gifBytes), color: Colors.orange, ), + const SizedBox(height: 32), + Align( + child: MyButton( + variant: MyButtonVariant.primaryMiddle, + onPressed: () => context.navPush(const StorageContentsView()), + child: Text(context.lang.settingsStorageContents), + ), + ), ], ), ); diff --git a/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart b/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart new file mode 100644 index 00000000..c8f00f6b --- /dev/null +++ b/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart @@ -0,0 +1,409 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:intl/intl.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/delete_memories_dialog.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; + +enum StorageSortOption { + size, + newest, + oldest, +} + +class StorageContentsView extends StatefulWidget { + const StorageContentsView({super.key}); + + @override + State<StorageContentsView> createState() => _StorageContentsViewState(); +} + +class _StorageContentsViewState extends State<StorageContentsView> { + StorageSortOption _sortOption = StorageSortOption.size; + final Set<String> _selectedMediaIds = {}; + late final Stream<List<MediaFile>> _mediaFilesStream; + + @override + void initState() { + super.initState(); + _mediaFilesStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles(); + } + + void _toggleSelection(String mediaId) { + setState(() { + if (_selectedMediaIds.contains(mediaId)) { + _selectedMediaIds.remove(mediaId); + } else { + _selectedMediaIds.add(mediaId); + } + }); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder<List<MediaFile>>( + stream: _mediaFilesStream, + builder: (context, snapshot) { + final allFiles = snapshot.data ?? []; + final storedFiles = allFiles.where((file) { + final ms = MediaFileService(file); + return file.stored || + (ms.storedPath.existsSync() && ms.storedPath.lengthSync() > 0); + }).toList(); + + // Sort the files + switch (_sortOption) { + case StorageSortOption.size: + storedFiles.sort( + (a, b) => + (b.sizeInBytes ?? 0).compareTo(a.sizeInBytes ?? 0), + ); + case StorageSortOption.newest: + storedFiles.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + case StorageSortOption.oldest: + storedFiles.sort((a, b) => a.createdAt.compareTo(b.createdAt)); + } + + final isSelecting = _selectedMediaIds.isNotEmpty; + + return Scaffold( + appBar: AppBar( + leading: isSelecting + ? IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(_selectedMediaIds.clear); + }, + ) + : null, + title: isSelecting + ? Text( + context.lang.memoriesSelectedCount(_selectedMediaIds.length), + style: const TextStyle(fontWeight: FontWeight.bold), + ) + : Text(context.lang.settingsStorageContents), + actions: [ + if (isSelecting) + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => _batchDelete(context, storedFiles), + ), + ], + ), + body: snapshot.connectionState == ConnectionState.waiting + ? const Center(child: CircularProgressIndicator()) + : snapshot.hasError + ? Center(child: Text('Error: ${snapshot.error}')) + : storedFiles.isEmpty + ? Center( + child: Text( + context.lang.settingsStorageNoContents, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: Colors.grey), + ), + ) + : Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + child: Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildChip( + StorageSortOption.size, + context.lang.settingsStorageSortStorage, + ), + const SizedBox(width: 8), + _buildChip( + StorageSortOption.newest, + context.lang.settingsStorageSortNewest, + ), + const SizedBox(width: 8), + _buildChip( + StorageSortOption.oldest, + context.lang.settingsStorageSortOldest, + ), + ], + ), + ), + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: storedFiles.length, + itemBuilder: (context, index) { + final file = storedFiles[index]; + return _buildListItem(context, file); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildChip(StorageSortOption option, String label) { + final isSelected = _sortOption == option; + return ChoiceChip( + label: Text(label), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setState(() { + _sortOption = option; + }); + } + }, + ); + } + + Widget _buildListItem(BuildContext context, MediaFile file) { + final sizeStr = formatBytes(file.sizeInBytes ?? 0); + final dateStr = DateFormat.yMMMd( + Localizations.localeOf(context).toString(), + ).format(file.createdAt); + + final isSelected = _selectedMediaIds.contains(file.mediaId); + final isSelecting = _selectedMediaIds.isNotEmpty; + + final ms = MediaFileService(file); + final hasStored = file.stored || + (ms.storedPath.existsSync() && ms.storedPath.lengthSync() > 0); + + final IconData statusIcon; + final String statusText; + + switch (file.cloudState) { + case CloudState.none: + statusIcon = Icons.cloud_off_outlined; + statusText = context.lang.settingsStorageLocalOnly; + case CloudState.pending: + statusIcon = Icons.cloud_upload_outlined; + statusText = context.lang.settingsStorageLocalOnly; + case CloudState.uploaded: + if (hasStored) { + statusIcon = Icons.cloud_done_outlined; + statusText = context.lang.settingsStorageLocalAndCloud; + } else { + statusIcon = Icons.cloud_outlined; + statusText = context.lang.settingsStorageCloudOnly; + } + } + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 8, + selected: isSelected, + onTap: () => _toggleSelection(file.mediaId), + onLongPress: () => _toggleSelection(file.mediaId), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isSelecting) ...[ + SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: isSelected, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (val) => _toggleSelection(file.mediaId), + ), + ), + const SizedBox(width: 8), + ], + _buildThumbnail(context, file), + ], + ), + title: Text('$sizeStr • $dateStr'), + subtitle: Row( + children: [ + Text( + file.type.name.toUpperCase(), + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + const SizedBox(width: 8), + Icon(statusIcon, size: 14, color: Colors.grey), + const SizedBox(width: 4), + Text( + statusText, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + trailing: isSelecting + ? null + : IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.redAccent), + onPressed: () => _deleteMemory(context, file), + ), + ); + } + + Widget _buildThumbnail(BuildContext context, MediaFile file) { + final ms = MediaFileService(file); + final isVideo = file.type == MediaType.video; + + final imgFile = ms.thumbnailPath.existsSync() && + ms.thumbnailPath.lengthSync() > 0 + ? ms.thumbnailPath + : ((file.type == MediaType.image || file.type == MediaType.gif) && + ms.storedPath.existsSync() && + ms.storedPath.lengthSync() > 0) + ? ms.storedPath + : (ms.tempPath.existsSync() && + ms.tempPath.lengthSync() > 0 && + !isVideo) + ? ms.tempPath + : null; + + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + width: 60, + height: 60, + color: Colors.grey.withValues(alpha: 0.1), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: imgFile != null + ? Image.file( + imgFile, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => const Icon( + Icons.broken_image_outlined, + size: 24, + color: Colors.grey, + ), + ) + : file.blurhash != null + ? BlurHash( + hash: file.blurhash!, + optimizationMode: + BlurHashOptimizationMode.approximation, + ) + : const Icon( + Icons.image_outlined, + size: 24, + color: Colors.grey, + ), + ), + if (isVideo) + Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(4), + child: const Icon( + Icons.play_arrow, + size: 16, + color: Colors.white, + ), + ), + ], + ), + ), + ); + } + + Future<void> _deleteMemory(BuildContext context, MediaFile file) async { + final hasCloudBackup = file.cloudState == CloudState.uploaded; + + final deleteCompletely = await showDeleteMemoriesDialog( + context: context, + count: 1, + hasCloudBackup: hasCloudBackup, + ); + + if (deleteCompletely == null) return; + + try { + MediaFileService(file).fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); + + if (deleteCompletely) { + unawaited(apiService.deleteMemory(file.mediaId)); + } + + if (context.mounted) { + showSnackbar( + context, + context.lang.memoriesDeleteSnackbarSuccess(1), + level: SnackbarLevel.success, + ); + } + } catch (e) { + if (context.mounted) { + showSnackbar( + context, + e.toString(), + ); + } + } + } + + Future<void> _batchDelete(BuildContext context, List<MediaFile> allFiles) async { + final selectedCount = _selectedMediaIds.length; + if (selectedCount == 0) return; + + final selectedFiles = allFiles + .where((file) => _selectedMediaIds.contains(file.mediaId)) + .toList(); + final hasAnyCloudBackup = + selectedFiles.any((file) => file.cloudState == CloudState.uploaded); + + final deleteCompletely = await showDeleteMemoriesDialog( + context: context, + count: selectedCount, + hasCloudBackup: hasAnyCloudBackup, + ); + + if (deleteCompletely == null) return; + + try { + for (final file in selectedFiles) { + MediaFileService(file).fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); + + if (deleteCompletely) { + unawaited(apiService.deleteMemory(file.mediaId)); + } + } + + setState(_selectedMediaIds.clear); + + if (context.mounted) { + showSnackbar( + context, + context.lang.memoriesDeleteSnackbarSuccess(selectedCount), + level: SnackbarLevel.success, + ); + } + } catch (e) { + if (context.mounted) { + showSnackbar( + context, + e.toString(), + ); + } + } + } +} From ffc88dfe7af961fa49521cd6c6ec3d004fa229ad Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Mon, 20 Jul 2026 20:56:52 +0200 Subject: [PATCH 27/29] disable memories --- lib/src/database/daos/mediafiles.dao.dart | 19 + .../generated/app_localizations.dart | 36 ++ .../generated/app_localizations_de.dart | 27 ++ .../generated/app_localizations_en.dart | 26 ++ lib/src/localization/translations | 2 +- .../api/websocket/client_to_server.pb.dart | 405 ++++++------------ .../websocket/client_to_server.pbjson.dart | 256 ++++------- lib/src/services/api/api.service.dart | 7 + .../memories/memories_cloud.service.dart | 8 +- .../data_and_storage/manage_storage.view.dart | 186 +++++++- .../storage_contents.view.dart | 173 ++++---- 11 files changed, 607 insertions(+), 538 deletions(-) diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 2e5d18dd..234eddf6 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -256,4 +256,23 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB> )) .get(); } + + Future<int> getCloudOnlyMemoriesCount() async { + final query = select(mediaFiles) + ..where( + (t) => + t.stored.equals(true) & + t.cloudState.equals(CloudState.uploaded.name), + ); + final rows = await query.get(); + + var count = 0; + for (final row in rows) { + final ms = MediaFileService(row); + if (!(ms.storedPath.existsSync() && ms.storedPath.lengthSync() > 0)) { + count++; + } + } + return count; + } } diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index df6aaf34..f27f2307 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -4237,6 +4237,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Local only'** String get settingsStorageLocalOnly; + + /// No description provided for @settingsStorageDisableBackupTitle. + /// + /// In en, this message translates to: + /// **'Disable Backup?'** + String get settingsStorageDisableBackupTitle; + + /// No description provided for @settingsStorageDisableBackupBody. + /// + /// In en, this message translates to: + /// **'This will delete all backed up memories from the cloud. There {count, plural, =1 {is *1 image*} other {are *{count} images*}} only stored in the cloud that will be *permanently lost*. Are you sure you want to proceed?'** + String settingsStorageDisableBackupBody(num count); + + /// No description provided for @settingsStorageDisableBackupBtn. + /// + /// In en, this message translates to: + /// **'Disable'** + String get settingsStorageDisableBackupBtn; + + /// No description provided for @settingsStorageDisableBackupAction. + /// + /// In en, this message translates to: + /// **'Disable Cloud Backup'** + String get settingsStorageDisableBackupAction; + + /// No description provided for @settingsStorageSyncNow. + /// + /// In en, this message translates to: + /// **'Sync now'** + String get settingsStorageSyncNow; + + /// No description provided for @settingsStorageSyncUpToDate. + /// + /// In en, this message translates to: + /// **'All memories are up to date.'** + String get settingsStorageSyncUpToDate; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index f8a094f0..c558af8e 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -2448,4 +2448,31 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsStorageLocalOnly => 'Nur lokal'; + + @override + String get settingsStorageDisableBackupTitle => 'Backup deaktivieren?'; + + @override + String settingsStorageDisableBackupBody(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '*$count Bilder*, die', + one: '*1 Bild*, das', + ); + return 'Dadurch werden alle gesicherten Erinnerungen aus der Cloud gelöscht. Es gibt $_temp0 nur in der Cloud und nicht lokal gespeichert sind. Diese gehen *dauerhaft verloren*. Fortfahren?'; + } + + @override + String get settingsStorageDisableBackupBtn => 'Deaktivieren'; + + @override + String get settingsStorageDisableBackupAction => 'Cloud-Backup deaktivieren'; + + @override + String get settingsStorageSyncNow => 'Jetzt synchronisieren'; + + @override + String get settingsStorageSyncUpToDate => + 'Alle Memories sind auf dem neuesten Stand.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 1b98deda..08afd8c0 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -2427,4 +2427,30 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsStorageLocalOnly => 'Local only'; + + @override + String get settingsStorageDisableBackupTitle => 'Disable Backup?'; + + @override + String settingsStorageDisableBackupBody(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'are *$count images*', + one: 'is *1 image*', + ); + return 'This will delete all backed up memories from the cloud. There $_temp0 only stored in the cloud that will be *permanently lost*. Are you sure you want to proceed?'; + } + + @override + String get settingsStorageDisableBackupBtn => 'Disable'; + + @override + String get settingsStorageDisableBackupAction => 'Disable Cloud Backup'; + + @override + String get settingsStorageSyncNow => 'Sync now'; + + @override + String get settingsStorageSyncUpToDate => 'All memories are up to date.'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index bcfb2b02..34c835db 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit bcfb2b02c59d1e3afe66b310296cb0f03eb75a72 +Subproject commit 34c835db2de08af962b39fb79ec11341b8847926 diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index d9d4e85c..96ada9c8 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -2472,47 +2472,6 @@ class ApplicationData_SetLoginToken extends $pb.GeneratedMessage { void clearLoginToken() => $_clearField(1); } -class ApplicationData_Deprecated extends $pb.GeneratedMessage { - factory ApplicationData_Deprecated() => create(); - - ApplicationData_Deprecated._(); - - factory ApplicationData_Deprecated.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory ApplicationData_Deprecated.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'ApplicationData.Deprecated', - package: - const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), - createEmptyInstance: create) - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_Deprecated clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - ApplicationData_Deprecated copyWith( - void Function(ApplicationData_Deprecated) updates) => - super.copyWith( - (message) => updates(message as ApplicationData_Deprecated)) - as ApplicationData_Deprecated; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static ApplicationData_Deprecated create() => ApplicationData_Deprecated._(); - @$core.override - ApplicationData_Deprecated createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static ApplicationData_Deprecated getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor<ApplicationData_Deprecated>(create); - static ApplicationData_Deprecated? _defaultInstance; -} - class ApplicationData_RegisterPasswordLessRecovery extends $pb.GeneratedMessage { factory ApplicationData_RegisterPasswordLessRecovery({ @@ -3054,23 +3013,59 @@ class ApplicationData_DeleteMemory extends $pb.GeneratedMessage { void clearMediaId() => $_clearField(1); } +class ApplicationData_DisableMemoriesBackup extends $pb.GeneratedMessage { + factory ApplicationData_DisableMemoriesBackup() => create(); + + ApplicationData_DisableMemoriesBackup._(); + + factory ApplicationData_DisableMemoriesBackup.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_DisableMemoriesBackup.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.DisableMemoriesBackup', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_DisableMemoriesBackup clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_DisableMemoriesBackup copyWith( + void Function(ApplicationData_DisableMemoriesBackup) updates) => + super.copyWith((message) => + updates(message as ApplicationData_DisableMemoriesBackup)) + as ApplicationData_DisableMemoriesBackup; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_DisableMemoriesBackup create() => + ApplicationData_DisableMemoriesBackup._(); + @$core.override + ApplicationData_DisableMemoriesBackup createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_DisableMemoriesBackup getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ApplicationData_DisableMemoriesBackup>(create); + static ApplicationData_DisableMemoriesBackup? _defaultInstance; +} + enum ApplicationData_ApplicationData { textMessage, getUserByUsername, getPrekeysByUserId, getUserById, updateGoogleFcmToken, - deprecated9, getCurrentPlanInfos, - deprecated11, getAvailablePlans, - deprecated13, - deprecated14, - deprecated15, - deprecated16, - deprecated17, removeAdditionalUser, - deprecated19, downloadDone, getSignedPrekeyByUserid, updateSignedPrekey, @@ -3089,6 +3084,7 @@ enum ApplicationData_ApplicationData { getMemoriesUrl, getMemoriesUsage, deleteMemory, + disableMemoriesBackup, notSet } @@ -3099,17 +3095,9 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetPrekeysByUserId? getPrekeysByUserId, ApplicationData_GetUserById? getUserById, ApplicationData_UpdateGoogleFcmToken? updateGoogleFcmToken, - ApplicationData_Deprecated? deprecated9, ApplicationData_GetCurrentPlanInfos? getCurrentPlanInfos, - ApplicationData_Deprecated? deprecated11, ApplicationData_GetAvailablePlans? getAvailablePlans, - ApplicationData_Deprecated? deprecated13, - ApplicationData_Deprecated? deprecated14, - ApplicationData_Deprecated? deprecated15, - ApplicationData_Deprecated? deprecated16, - ApplicationData_Deprecated? deprecated17, ApplicationData_RemoveAdditionalUser? removeAdditionalUser, - ApplicationData_Deprecated? deprecated19, ApplicationData_DownloadDone? downloadDone, ApplicationData_GetSignedPreKeyByUserId? getSignedPrekeyByUserid, ApplicationData_UpdateSignedPreKey? updateSignedPrekey, @@ -3128,6 +3116,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_GetMemoriesUrl? getMemoriesUrl, ApplicationData_GetMemoriesUsage? getMemoriesUsage, ApplicationData_DeleteMemory? deleteMemory, + ApplicationData_DisableMemoriesBackup? disableMemoriesBackup, }) { final result = create(); if (textMessage != null) result.textMessage = textMessage; @@ -3137,19 +3126,11 @@ class ApplicationData extends $pb.GeneratedMessage { if (getUserById != null) result.getUserById = getUserById; if (updateGoogleFcmToken != null) result.updateGoogleFcmToken = updateGoogleFcmToken; - if (deprecated9 != null) result.deprecated9 = deprecated9; if (getCurrentPlanInfos != null) result.getCurrentPlanInfos = getCurrentPlanInfos; - if (deprecated11 != null) result.deprecated11 = deprecated11; if (getAvailablePlans != null) result.getAvailablePlans = getAvailablePlans; - if (deprecated13 != null) result.deprecated13 = deprecated13; - if (deprecated14 != null) result.deprecated14 = deprecated14; - if (deprecated15 != null) result.deprecated15 = deprecated15; - if (deprecated16 != null) result.deprecated16 = deprecated16; - if (deprecated17 != null) result.deprecated17 = deprecated17; if (removeAdditionalUser != null) result.removeAdditionalUser = removeAdditionalUser; - if (deprecated19 != null) result.deprecated19 = deprecated19; if (downloadDone != null) result.downloadDone = downloadDone; if (getSignedPrekeyByUserid != null) result.getSignedPrekeyByUserid = getSignedPrekeyByUserid; @@ -3174,6 +3155,8 @@ class ApplicationData extends $pb.GeneratedMessage { if (getMemoriesUrl != null) result.getMemoriesUrl = getMemoriesUrl; if (getMemoriesUsage != null) result.getMemoriesUsage = getMemoriesUsage; if (deleteMemory != null) result.deleteMemory = deleteMemory; + if (disableMemoriesBackup != null) + result.disableMemoriesBackup = disableMemoriesBackup; return result; } @@ -3193,17 +3176,9 @@ class ApplicationData extends $pb.GeneratedMessage { 3: ApplicationData_ApplicationData.getPrekeysByUserId, 6: ApplicationData_ApplicationData.getUserById, 8: ApplicationData_ApplicationData.updateGoogleFcmToken, - 9: ApplicationData_ApplicationData.deprecated9, 10: ApplicationData_ApplicationData.getCurrentPlanInfos, - 11: ApplicationData_ApplicationData.deprecated11, 12: ApplicationData_ApplicationData.getAvailablePlans, - 13: ApplicationData_ApplicationData.deprecated13, - 14: ApplicationData_ApplicationData.deprecated14, - 15: ApplicationData_ApplicationData.deprecated15, - 16: ApplicationData_ApplicationData.deprecated16, - 17: ApplicationData_ApplicationData.deprecated17, 18: ApplicationData_ApplicationData.removeAdditionalUser, - 19: ApplicationData_ApplicationData.deprecated19, 20: ApplicationData_ApplicationData.downloadDone, 22: ApplicationData_ApplicationData.getSignedPrekeyByUserid, 23: ApplicationData_ApplicationData.updateSignedPrekey, @@ -3222,6 +3197,7 @@ class ApplicationData extends $pb.GeneratedMessage { 36: ApplicationData_ApplicationData.getMemoriesUrl, 37: ApplicationData_ApplicationData.getMemoriesUsage, 38: ApplicationData_ApplicationData.deleteMemory, + 39: ApplicationData_ApplicationData.disableMemoriesBackup, 0: ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -3235,17 +3211,9 @@ class ApplicationData extends $pb.GeneratedMessage { 3, 6, 8, - 9, 10, - 11, 12, - 13, - 14, - 15, - 16, - 17, 18, - 19, 20, 22, 23, @@ -3263,7 +3231,8 @@ class ApplicationData extends $pb.GeneratedMessage { 35, 36, 37, - 38 + 38, + 39 ]) ..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', @@ -3283,42 +3252,18 @@ class ApplicationData extends $pb.GeneratedMessage { 8, _omitFieldNames ? '' : 'updateGoogleFcmToken', protoName: 'updateGoogleFcmToken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) - ..aOM<ApplicationData_Deprecated>(9, _omitFieldNames ? '' : 'deprecated9', - protoName: 'deprecated_9', - subBuilder: ApplicationData_Deprecated.create) ..aOM<ApplicationData_GetCurrentPlanInfos>( 10, _omitFieldNames ? '' : 'getCurrentPlanInfos', protoName: 'getCurrentPlanInfos', subBuilder: ApplicationData_GetCurrentPlanInfos.create) - ..aOM<ApplicationData_Deprecated>(11, _omitFieldNames ? '' : 'deprecated11', - protoName: 'deprecated_11', - subBuilder: ApplicationData_Deprecated.create) ..aOM<ApplicationData_GetAvailablePlans>( 12, _omitFieldNames ? '' : 'getAvailablePlans', protoName: 'getAvailablePlans', subBuilder: ApplicationData_GetAvailablePlans.create) - ..aOM<ApplicationData_Deprecated>(13, _omitFieldNames ? '' : 'deprecated13', - protoName: 'deprecated_13', - subBuilder: ApplicationData_Deprecated.create) - ..aOM<ApplicationData_Deprecated>(14, _omitFieldNames ? '' : 'deprecated14', - protoName: 'deprecated_14', - subBuilder: ApplicationData_Deprecated.create) - ..aOM<ApplicationData_Deprecated>(15, _omitFieldNames ? '' : 'deprecated15', - protoName: 'deprecated_15', - subBuilder: ApplicationData_Deprecated.create) - ..aOM<ApplicationData_Deprecated>(16, _omitFieldNames ? '' : 'deprecated16', - protoName: 'deprecated_16', - subBuilder: ApplicationData_Deprecated.create) - ..aOM<ApplicationData_Deprecated>(17, _omitFieldNames ? '' : 'deprecated17', - protoName: 'deprecated_17', - subBuilder: ApplicationData_Deprecated.create) ..aOM<ApplicationData_RemoveAdditionalUser>( 18, _omitFieldNames ? '' : 'removeAdditionalUser', protoName: 'removeAdditionalUser', subBuilder: ApplicationData_RemoveAdditionalUser.create) - ..aOM<ApplicationData_Deprecated>(19, _omitFieldNames ? '' : 'deprecated19', - protoName: 'deprecated_19', - subBuilder: ApplicationData_Deprecated.create) ..aOM<ApplicationData_DownloadDone>( 20, _omitFieldNames ? '' : 'downloadDone', protoName: 'downloadDone', @@ -3379,6 +3324,9 @@ class ApplicationData extends $pb.GeneratedMessage { ..aOM<ApplicationData_DeleteMemory>( 38, _omitFieldNames ? '' : 'deleteMemory', subBuilder: ApplicationData_DeleteMemory.create) + ..aOM<ApplicationData_DisableMemoriesBackup>( + 39, _omitFieldNames ? '' : 'disableMemoriesBackup', + subBuilder: ApplicationData_DisableMemoriesBackup.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -3405,17 +3353,9 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(3) @$pb.TagNumber(6) @$pb.TagNumber(8) - @$pb.TagNumber(9) @$pb.TagNumber(10) - @$pb.TagNumber(11) @$pb.TagNumber(12) - @$pb.TagNumber(13) - @$pb.TagNumber(14) - @$pb.TagNumber(15) - @$pb.TagNumber(16) - @$pb.TagNumber(17) @$pb.TagNumber(18) - @$pb.TagNumber(19) @$pb.TagNumber(20) @$pb.TagNumber(22) @$pb.TagNumber(23) @@ -3434,6 +3374,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(36) @$pb.TagNumber(37) @$pb.TagNumber(38) + @$pb.TagNumber(39) ApplicationData_ApplicationData whichApplicationData() => _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -3441,17 +3382,9 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(3) @$pb.TagNumber(6) @$pb.TagNumber(8) - @$pb.TagNumber(9) @$pb.TagNumber(10) - @$pb.TagNumber(11) @$pb.TagNumber(12) - @$pb.TagNumber(13) - @$pb.TagNumber(14) - @$pb.TagNumber(15) - @$pb.TagNumber(16) - @$pb.TagNumber(17) @$pb.TagNumber(18) - @$pb.TagNumber(19) @$pb.TagNumber(20) @$pb.TagNumber(22) @$pb.TagNumber(23) @@ -3470,6 +3403,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(36) @$pb.TagNumber(37) @$pb.TagNumber(38) + @$pb.TagNumber(39) void clearApplicationData() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -3531,353 +3465,278 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_UpdateGoogleFcmToken ensureUpdateGoogleFcmToken() => $_ensure(4); - @$pb.TagNumber(9) - ApplicationData_Deprecated get deprecated9 => $_getN(5); - @$pb.TagNumber(9) - set deprecated9(ApplicationData_Deprecated value) => $_setField(9, value); - @$pb.TagNumber(9) - $core.bool hasDeprecated9() => $_has(5); - @$pb.TagNumber(9) - void clearDeprecated9() => $_clearField(9); - @$pb.TagNumber(9) - ApplicationData_Deprecated ensureDeprecated9() => $_ensure(5); - @$pb.TagNumber(10) - ApplicationData_GetCurrentPlanInfos get getCurrentPlanInfos => $_getN(6); + ApplicationData_GetCurrentPlanInfos get getCurrentPlanInfos => $_getN(5); @$pb.TagNumber(10) set getCurrentPlanInfos(ApplicationData_GetCurrentPlanInfos value) => $_setField(10, value); @$pb.TagNumber(10) - $core.bool hasGetCurrentPlanInfos() => $_has(6); + $core.bool hasGetCurrentPlanInfos() => $_has(5); @$pb.TagNumber(10) void clearGetCurrentPlanInfos() => $_clearField(10); @$pb.TagNumber(10) ApplicationData_GetCurrentPlanInfos ensureGetCurrentPlanInfos() => - $_ensure(6); - - @$pb.TagNumber(11) - ApplicationData_Deprecated get deprecated11 => $_getN(7); - @$pb.TagNumber(11) - set deprecated11(ApplicationData_Deprecated value) => $_setField(11, value); - @$pb.TagNumber(11) - $core.bool hasDeprecated11() => $_has(7); - @$pb.TagNumber(11) - void clearDeprecated11() => $_clearField(11); - @$pb.TagNumber(11) - ApplicationData_Deprecated ensureDeprecated11() => $_ensure(7); + $_ensure(5); @$pb.TagNumber(12) - ApplicationData_GetAvailablePlans get getAvailablePlans => $_getN(8); + ApplicationData_GetAvailablePlans get getAvailablePlans => $_getN(6); @$pb.TagNumber(12) set getAvailablePlans(ApplicationData_GetAvailablePlans value) => $_setField(12, value); @$pb.TagNumber(12) - $core.bool hasGetAvailablePlans() => $_has(8); + $core.bool hasGetAvailablePlans() => $_has(6); @$pb.TagNumber(12) void clearGetAvailablePlans() => $_clearField(12); @$pb.TagNumber(12) - ApplicationData_GetAvailablePlans ensureGetAvailablePlans() => $_ensure(8); - - @$pb.TagNumber(13) - ApplicationData_Deprecated get deprecated13 => $_getN(9); - @$pb.TagNumber(13) - set deprecated13(ApplicationData_Deprecated value) => $_setField(13, value); - @$pb.TagNumber(13) - $core.bool hasDeprecated13() => $_has(9); - @$pb.TagNumber(13) - void clearDeprecated13() => $_clearField(13); - @$pb.TagNumber(13) - ApplicationData_Deprecated ensureDeprecated13() => $_ensure(9); - - @$pb.TagNumber(14) - ApplicationData_Deprecated get deprecated14 => $_getN(10); - @$pb.TagNumber(14) - set deprecated14(ApplicationData_Deprecated value) => $_setField(14, value); - @$pb.TagNumber(14) - $core.bool hasDeprecated14() => $_has(10); - @$pb.TagNumber(14) - void clearDeprecated14() => $_clearField(14); - @$pb.TagNumber(14) - ApplicationData_Deprecated ensureDeprecated14() => $_ensure(10); - - @$pb.TagNumber(15) - ApplicationData_Deprecated get deprecated15 => $_getN(11); - @$pb.TagNumber(15) - set deprecated15(ApplicationData_Deprecated value) => $_setField(15, value); - @$pb.TagNumber(15) - $core.bool hasDeprecated15() => $_has(11); - @$pb.TagNumber(15) - void clearDeprecated15() => $_clearField(15); - @$pb.TagNumber(15) - ApplicationData_Deprecated ensureDeprecated15() => $_ensure(11); - - @$pb.TagNumber(16) - ApplicationData_Deprecated get deprecated16 => $_getN(12); - @$pb.TagNumber(16) - set deprecated16(ApplicationData_Deprecated value) => $_setField(16, value); - @$pb.TagNumber(16) - $core.bool hasDeprecated16() => $_has(12); - @$pb.TagNumber(16) - void clearDeprecated16() => $_clearField(16); - @$pb.TagNumber(16) - ApplicationData_Deprecated ensureDeprecated16() => $_ensure(12); - - @$pb.TagNumber(17) - ApplicationData_Deprecated get deprecated17 => $_getN(13); - @$pb.TagNumber(17) - set deprecated17(ApplicationData_Deprecated value) => $_setField(17, value); - @$pb.TagNumber(17) - $core.bool hasDeprecated17() => $_has(13); - @$pb.TagNumber(17) - void clearDeprecated17() => $_clearField(17); - @$pb.TagNumber(17) - ApplicationData_Deprecated ensureDeprecated17() => $_ensure(13); + ApplicationData_GetAvailablePlans ensureGetAvailablePlans() => $_ensure(6); @$pb.TagNumber(18) - ApplicationData_RemoveAdditionalUser get removeAdditionalUser => $_getN(14); + ApplicationData_RemoveAdditionalUser get removeAdditionalUser => $_getN(7); @$pb.TagNumber(18) set removeAdditionalUser(ApplicationData_RemoveAdditionalUser value) => $_setField(18, value); @$pb.TagNumber(18) - $core.bool hasRemoveAdditionalUser() => $_has(14); + $core.bool hasRemoveAdditionalUser() => $_has(7); @$pb.TagNumber(18) void clearRemoveAdditionalUser() => $_clearField(18); @$pb.TagNumber(18) ApplicationData_RemoveAdditionalUser ensureRemoveAdditionalUser() => - $_ensure(14); - - @$pb.TagNumber(19) - ApplicationData_Deprecated get deprecated19 => $_getN(15); - @$pb.TagNumber(19) - set deprecated19(ApplicationData_Deprecated value) => $_setField(19, value); - @$pb.TagNumber(19) - $core.bool hasDeprecated19() => $_has(15); - @$pb.TagNumber(19) - void clearDeprecated19() => $_clearField(19); - @$pb.TagNumber(19) - ApplicationData_Deprecated ensureDeprecated19() => $_ensure(15); + $_ensure(7); @$pb.TagNumber(20) - ApplicationData_DownloadDone get downloadDone => $_getN(16); + ApplicationData_DownloadDone get downloadDone => $_getN(8); @$pb.TagNumber(20) set downloadDone(ApplicationData_DownloadDone value) => $_setField(20, value); @$pb.TagNumber(20) - $core.bool hasDownloadDone() => $_has(16); + $core.bool hasDownloadDone() => $_has(8); @$pb.TagNumber(20) void clearDownloadDone() => $_clearField(20); @$pb.TagNumber(20) - ApplicationData_DownloadDone ensureDownloadDone() => $_ensure(16); + ApplicationData_DownloadDone ensureDownloadDone() => $_ensure(8); @$pb.TagNumber(22) ApplicationData_GetSignedPreKeyByUserId get getSignedPrekeyByUserid => - $_getN(17); + $_getN(9); @$pb.TagNumber(22) set getSignedPrekeyByUserid(ApplicationData_GetSignedPreKeyByUserId value) => $_setField(22, value); @$pb.TagNumber(22) - $core.bool hasGetSignedPrekeyByUserid() => $_has(17); + $core.bool hasGetSignedPrekeyByUserid() => $_has(9); @$pb.TagNumber(22) void clearGetSignedPrekeyByUserid() => $_clearField(22); @$pb.TagNumber(22) ApplicationData_GetSignedPreKeyByUserId ensureGetSignedPrekeyByUserid() => - $_ensure(17); + $_ensure(9); @$pb.TagNumber(23) - ApplicationData_UpdateSignedPreKey get updateSignedPrekey => $_getN(18); + ApplicationData_UpdateSignedPreKey get updateSignedPrekey => $_getN(10); @$pb.TagNumber(23) set updateSignedPrekey(ApplicationData_UpdateSignedPreKey value) => $_setField(23, value); @$pb.TagNumber(23) - $core.bool hasUpdateSignedPrekey() => $_has(18); + $core.bool hasUpdateSignedPrekey() => $_has(10); @$pb.TagNumber(23) void clearUpdateSignedPrekey() => $_clearField(23); @$pb.TagNumber(23) - ApplicationData_UpdateSignedPreKey ensureUpdateSignedPrekey() => $_ensure(18); + ApplicationData_UpdateSignedPreKey ensureUpdateSignedPrekey() => $_ensure(10); @$pb.TagNumber(24) - ApplicationData_DeleteAccount get deleteAccount => $_getN(19); + ApplicationData_DeleteAccount get deleteAccount => $_getN(11); @$pb.TagNumber(24) set deleteAccount(ApplicationData_DeleteAccount value) => $_setField(24, value); @$pb.TagNumber(24) - $core.bool hasDeleteAccount() => $_has(19); + $core.bool hasDeleteAccount() => $_has(11); @$pb.TagNumber(24) void clearDeleteAccount() => $_clearField(24); @$pb.TagNumber(24) - ApplicationData_DeleteAccount ensureDeleteAccount() => $_ensure(19); + ApplicationData_DeleteAccount ensureDeleteAccount() => $_ensure(11); @$pb.TagNumber(25) - ApplicationData_ReportUser get reportUser => $_getN(20); + ApplicationData_ReportUser get reportUser => $_getN(12); @$pb.TagNumber(25) set reportUser(ApplicationData_ReportUser value) => $_setField(25, value); @$pb.TagNumber(25) - $core.bool hasReportUser() => $_has(20); + $core.bool hasReportUser() => $_has(12); @$pb.TagNumber(25) void clearReportUser() => $_clearField(25); @$pb.TagNumber(25) - ApplicationData_ReportUser ensureReportUser() => $_ensure(20); + ApplicationData_ReportUser ensureReportUser() => $_ensure(12); @$pb.TagNumber(26) - ApplicationData_ChangeUsername get changeUsername => $_getN(21); + ApplicationData_ChangeUsername get changeUsername => $_getN(13); @$pb.TagNumber(26) set changeUsername(ApplicationData_ChangeUsername value) => $_setField(26, value); @$pb.TagNumber(26) - $core.bool hasChangeUsername() => $_has(21); + $core.bool hasChangeUsername() => $_has(13); @$pb.TagNumber(26) void clearChangeUsername() => $_clearField(26); @$pb.TagNumber(26) - ApplicationData_ChangeUsername ensureChangeUsername() => $_ensure(21); + ApplicationData_ChangeUsername ensureChangeUsername() => $_ensure(13); @$pb.TagNumber(27) - ApplicationData_IPAPurchase get ipaPurchase => $_getN(22); + ApplicationData_IPAPurchase get ipaPurchase => $_getN(14); @$pb.TagNumber(27) set ipaPurchase(ApplicationData_IPAPurchase value) => $_setField(27, value); @$pb.TagNumber(27) - $core.bool hasIpaPurchase() => $_has(22); + $core.bool hasIpaPurchase() => $_has(14); @$pb.TagNumber(27) void clearIpaPurchase() => $_clearField(27); @$pb.TagNumber(27) - ApplicationData_IPAPurchase ensureIpaPurchase() => $_ensure(22); + ApplicationData_IPAPurchase ensureIpaPurchase() => $_ensure(14); @$pb.TagNumber(28) - ApplicationData_IPAForceCheck get ipaForceCheck => $_getN(23); + ApplicationData_IPAForceCheck get ipaForceCheck => $_getN(15); @$pb.TagNumber(28) set ipaForceCheck(ApplicationData_IPAForceCheck value) => $_setField(28, value); @$pb.TagNumber(28) - $core.bool hasIpaForceCheck() => $_has(23); + $core.bool hasIpaForceCheck() => $_has(15); @$pb.TagNumber(28) void clearIpaForceCheck() => $_clearField(28); @$pb.TagNumber(28) - ApplicationData_IPAForceCheck ensureIpaForceCheck() => $_ensure(23); + ApplicationData_IPAForceCheck ensureIpaForceCheck() => $_ensure(15); @$pb.TagNumber(29) - ApplicationData_AddAdditionalUser get addAdditionalUser => $_getN(24); + ApplicationData_AddAdditionalUser get addAdditionalUser => $_getN(16); @$pb.TagNumber(29) set addAdditionalUser(ApplicationData_AddAdditionalUser value) => $_setField(29, value); @$pb.TagNumber(29) - $core.bool hasAddAdditionalUser() => $_has(24); + $core.bool hasAddAdditionalUser() => $_has(16); @$pb.TagNumber(29) void clearAddAdditionalUser() => $_clearField(29); @$pb.TagNumber(29) - ApplicationData_AddAdditionalUser ensureAddAdditionalUser() => $_ensure(24); + ApplicationData_AddAdditionalUser ensureAddAdditionalUser() => $_ensure(16); @$pb.TagNumber(30) - ApplicationData_SetLoginToken get setLoginToken => $_getN(25); + ApplicationData_SetLoginToken get setLoginToken => $_getN(17); @$pb.TagNumber(30) set setLoginToken(ApplicationData_SetLoginToken value) => $_setField(30, value); @$pb.TagNumber(30) - $core.bool hasSetLoginToken() => $_has(25); + $core.bool hasSetLoginToken() => $_has(17); @$pb.TagNumber(30) void clearSetLoginToken() => $_clearField(30); @$pb.TagNumber(30) - ApplicationData_SetLoginToken ensureSetLoginToken() => $_ensure(25); + ApplicationData_SetLoginToken ensureSetLoginToken() => $_ensure(17); @$pb.TagNumber(31) ApplicationData_RegisterPasswordLessRecovery - get registerPasswordlessRecovery => $_getN(26); + get registerPasswordlessRecovery => $_getN(18); @$pb.TagNumber(31) set registerPasswordlessRecovery( ApplicationData_RegisterPasswordLessRecovery value) => $_setField(31, value); @$pb.TagNumber(31) - $core.bool hasRegisterPasswordlessRecovery() => $_has(26); + $core.bool hasRegisterPasswordlessRecovery() => $_has(18); @$pb.TagNumber(31) void clearRegisterPasswordlessRecovery() => $_clearField(31); @$pb.TagNumber(31) ApplicationData_RegisterPasswordLessRecovery - ensureRegisterPasswordlessRecovery() => $_ensure(26); + ensureRegisterPasswordlessRecovery() => $_ensure(18); @$pb.TagNumber(32) ApplicationData_PasswordlessNotification get passwordlessNotification => - $_getN(27); + $_getN(19); @$pb.TagNumber(32) set passwordlessNotification( ApplicationData_PasswordlessNotification value) => $_setField(32, value); @$pb.TagNumber(32) - $core.bool hasPasswordlessNotification() => $_has(27); + $core.bool hasPasswordlessNotification() => $_has(19); @$pb.TagNumber(32) void clearPasswordlessNotification() => $_clearField(32); @$pb.TagNumber(32) ApplicationData_PasswordlessNotification ensurePasswordlessNotification() => - $_ensure(27); + $_ensure(19); @$pb.TagNumber(33) - ApplicationData_RequestMemoriesUpload get requestMemoriesUpload => $_getN(28); + ApplicationData_RequestMemoriesUpload get requestMemoriesUpload => $_getN(20); @$pb.TagNumber(33) set requestMemoriesUpload(ApplicationData_RequestMemoriesUpload value) => $_setField(33, value); @$pb.TagNumber(33) - $core.bool hasRequestMemoriesUpload() => $_has(28); + $core.bool hasRequestMemoriesUpload() => $_has(20); @$pb.TagNumber(33) void clearRequestMemoriesUpload() => $_clearField(33); @$pb.TagNumber(33) ApplicationData_RequestMemoriesUpload ensureRequestMemoriesUpload() => - $_ensure(28); + $_ensure(20); @$pb.TagNumber(34) - ApplicationData_ConfirmMemoriesUpload get confirmMemoriesUpload => $_getN(29); + ApplicationData_ConfirmMemoriesUpload get confirmMemoriesUpload => $_getN(21); @$pb.TagNumber(34) set confirmMemoriesUpload(ApplicationData_ConfirmMemoriesUpload value) => $_setField(34, value); @$pb.TagNumber(34) - $core.bool hasConfirmMemoriesUpload() => $_has(29); + $core.bool hasConfirmMemoriesUpload() => $_has(21); @$pb.TagNumber(34) void clearConfirmMemoriesUpload() => $_clearField(34); @$pb.TagNumber(34) ApplicationData_ConfirmMemoriesUpload ensureConfirmMemoriesUpload() => - $_ensure(29); + $_ensure(21); @$pb.TagNumber(35) - ApplicationData_GetMemoriesList get getMemoriesList => $_getN(30); + ApplicationData_GetMemoriesList get getMemoriesList => $_getN(22); @$pb.TagNumber(35) set getMemoriesList(ApplicationData_GetMemoriesList value) => $_setField(35, value); @$pb.TagNumber(35) - $core.bool hasGetMemoriesList() => $_has(30); + $core.bool hasGetMemoriesList() => $_has(22); @$pb.TagNumber(35) void clearGetMemoriesList() => $_clearField(35); @$pb.TagNumber(35) - ApplicationData_GetMemoriesList ensureGetMemoriesList() => $_ensure(30); + ApplicationData_GetMemoriesList ensureGetMemoriesList() => $_ensure(22); @$pb.TagNumber(36) - ApplicationData_GetMemoriesUrl get getMemoriesUrl => $_getN(31); + ApplicationData_GetMemoriesUrl get getMemoriesUrl => $_getN(23); @$pb.TagNumber(36) set getMemoriesUrl(ApplicationData_GetMemoriesUrl value) => $_setField(36, value); @$pb.TagNumber(36) - $core.bool hasGetMemoriesUrl() => $_has(31); + $core.bool hasGetMemoriesUrl() => $_has(23); @$pb.TagNumber(36) void clearGetMemoriesUrl() => $_clearField(36); @$pb.TagNumber(36) - ApplicationData_GetMemoriesUrl ensureGetMemoriesUrl() => $_ensure(31); + ApplicationData_GetMemoriesUrl ensureGetMemoriesUrl() => $_ensure(23); @$pb.TagNumber(37) - ApplicationData_GetMemoriesUsage get getMemoriesUsage => $_getN(32); + ApplicationData_GetMemoriesUsage get getMemoriesUsage => $_getN(24); @$pb.TagNumber(37) set getMemoriesUsage(ApplicationData_GetMemoriesUsage value) => $_setField(37, value); @$pb.TagNumber(37) - $core.bool hasGetMemoriesUsage() => $_has(32); + $core.bool hasGetMemoriesUsage() => $_has(24); @$pb.TagNumber(37) void clearGetMemoriesUsage() => $_clearField(37); @$pb.TagNumber(37) - ApplicationData_GetMemoriesUsage ensureGetMemoriesUsage() => $_ensure(32); + ApplicationData_GetMemoriesUsage ensureGetMemoriesUsage() => $_ensure(24); @$pb.TagNumber(38) - ApplicationData_DeleteMemory get deleteMemory => $_getN(33); + ApplicationData_DeleteMemory get deleteMemory => $_getN(25); @$pb.TagNumber(38) set deleteMemory(ApplicationData_DeleteMemory value) => $_setField(38, value); @$pb.TagNumber(38) - $core.bool hasDeleteMemory() => $_has(33); + $core.bool hasDeleteMemory() => $_has(25); @$pb.TagNumber(38) void clearDeleteMemory() => $_clearField(38); @$pb.TagNumber(38) - ApplicationData_DeleteMemory ensureDeleteMemory() => $_ensure(33); + ApplicationData_DeleteMemory ensureDeleteMemory() => $_ensure(25); + + @$pb.TagNumber(39) + ApplicationData_DisableMemoriesBackup get disableMemoriesBackup => $_getN(26); + @$pb.TagNumber(39) + set disableMemoriesBackup(ApplicationData_DisableMemoriesBackup value) => + $_setField(39, value); + @$pb.TagNumber(39) + $core.bool hasDisableMemoriesBackup() => $_has(26); + @$pb.TagNumber(39) + void clearDisableMemoriesBackup() => $_clearField(39); + @$pb.TagNumber(39) + ApplicationData_DisableMemoriesBackup ensureDisableMemoriesBackup() => + $_ensure(26); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index f9fa85c5..7245b957 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -526,15 +526,6 @@ const ApplicationData$json = { '9': 0, '10': 'updateGoogleFcmToken' }, - { - '1': 'deprecated_9', - '3': 9, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated9' - }, { '1': 'getCurrentPlanInfos', '3': 10, @@ -544,15 +535,6 @@ const ApplicationData$json = { '9': 0, '10': 'getCurrentPlanInfos' }, - { - '1': 'deprecated_11', - '3': 11, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated11' - }, { '1': 'getAvailablePlans', '3': 12, @@ -562,60 +544,6 @@ const ApplicationData$json = { '9': 0, '10': 'getAvailablePlans' }, - { - '1': 'deprecated_13', - '3': 13, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated13' - }, - { - '1': 'deprecated_14', - '3': 14, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated14' - }, - { - '1': 'deprecated_15', - '3': 15, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated15' - }, - { - '1': 'deprecated_16', - '3': 16, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated16' - }, - { - '1': 'deprecated_17', - '3': 17, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated17' - }, - { - '1': 'deprecated_19', - '3': 19, - '4': 1, - '5': 11, - '6': '.client_to_server.ApplicationData.Deprecated', - '9': 0, - '10': 'deprecated19' - }, { '1': 'downloadDone', '3': 20, @@ -787,6 +715,15 @@ const ApplicationData$json = { '9': 0, '10': 'deleteMemory' }, + { + '1': 'disable_memories_backup', + '3': 39, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.DisableMemoriesBackup', + '9': 0, + '10': 'disableMemoriesBackup' + }, ], '3': [ ApplicationData_TextMessage$json, @@ -807,7 +744,6 @@ const ApplicationData$json = { ApplicationData_DeleteAccount$json, ApplicationData_AddAdditionalUser$json, ApplicationData_SetLoginToken$json, - ApplicationData_Deprecated$json, ApplicationData_RegisterPasswordLessRecovery$json, ApplicationData_PasswordlessNotification$json, ApplicationData_RequestMemoriesUpload$json, @@ -815,11 +751,18 @@ const ApplicationData$json = { ApplicationData_GetMemoriesList$json, ApplicationData_GetMemoriesUrl$json, ApplicationData_GetMemoriesUsage$json, - ApplicationData_DeleteMemory$json + ApplicationData_DeleteMemory$json, + ApplicationData_DisableMemoriesBackup$json ], '8': [ {'1': 'ApplicationData'}, ], + '9': [ + {'1': 9, '2': 10}, + {'1': 11, '2': 12}, + {'1': 13, '2': 18}, + {'1': 19, '2': 20}, + ], }; @$core.Deprecated('Use applicationDataDescriptor instead') @@ -984,11 +927,6 @@ const ApplicationData_SetLoginToken$json = { ], }; -@$core.Deprecated('Use applicationDataDescriptor instead') -const ApplicationData_Deprecated$json = { - '1': 'Deprecated', -}; - @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_RegisterPasswordLessRecovery$json = { '1': 'RegisterPasswordLessRecovery', @@ -1079,6 +1017,11 @@ const ApplicationData_DeleteMemory$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_DisableMemoriesBackup$json = { + '1': 'DisableMemoriesBackup', +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' @@ -1089,88 +1032,79 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'VzZXJJZEgAUhJnZXRQcmVrZXlzQnlVc2VySWQSUQoLZ2V0VXNlckJ5SWQYBiABKAsyLS5jbGll' 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB' 'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' - 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl' - 'EKDGRlcHJlY2F0ZWRfORgJIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRh' - 'LkRlcHJlY2F0ZWRIAFILZGVwcmVjYXRlZDkSaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz' - 'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI' - 'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJTCg1kZXByZWNhdGVkXzExGAsgASgLMiwuY2xpZW50X3' - 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESYwoR' - 'Z2V0QXZhaWxhYmxlUGxhbnMYDCABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRG' - 'F0YS5HZXRBdmFpbGFibGVQbGFuc0gAUhFnZXRBdmFpbGFibGVQbGFucxJTCg1kZXByZWNhdGVk' - 'XzEzGA0gASgLMiwuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZE' - 'gAUgxkZXByZWNhdGVkMTMSUwoNZGVwcmVjYXRlZF8xNBgOIAEoCzIsLmNsaWVudF90b19zZXJ2' - 'ZXIuQXBwbGljYXRpb25EYXRhLkRlcHJlY2F0ZWRIAFIMZGVwcmVjYXRlZDE0ElMKDWRlcHJlY2' - 'F0ZWRfMTUYDyABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNh' - 'dGVkSABSDGRlcHJlY2F0ZWQxNRJTCg1kZXByZWNhdGVkXzE2GBAgASgLMiwuY2xpZW50X3RvX3' - 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTYSUwoNZGVw' - 'cmVjYXRlZF8xNxgRIAEoCzIsLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlcH' - 'JlY2F0ZWRIAFIMZGVwcmVjYXRlZDE3ElMKDWRlcHJlY2F0ZWRfMTkYEyABKAsyLC5jbGllbnRf' - 'dG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxORJUCg' - 'xkb3dubG9hZERvbmUYFCABKAsyLi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5E' - 'b3dubG9hZERvbmVIAFIMZG93bmxvYWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGB' - 'YgASgLMjkuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5' - 'QnlVc2VySWRIAFIXZ2V0U2lnbmVkUHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2' - 'V5GBcgASgLMjQuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVk' - 'UHJlS2V5SABSEnVwZGF0ZVNpZ25lZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2' - 'xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVB' - 'Y2NvdW50Ek4KCnJlcG9ydFVzZXIYGSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW' - '9uRGF0YS5SZXBvcnRVc2VySABSCnJlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsy' - 'MC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaG' - 'FuZ2VVc2VybmFtZRJRCgtpcGFQdXJjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBw' - 'bGljYXRpb25EYXRhLklQQVB1cmNoYXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2' - 'sYHCABKAsyLy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNr' - 'SABSDWlwYUZvcmNlQ2hlY2sSbAoUcmVtb3ZlQWRkaXRpb25hbFVzZXIYEiABKAsyNi5jbGllbn' - 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZW1vdmVBZGRpdGlvbmFsVXNlckgAUhRyZW1v' - 'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1' - '9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u' - 'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG' - 'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhKGAQoecmVnaXN0ZXJf' - 'cGFzc3dvcmRsZXNzX3JlY292ZXJ5GB8gASgLMj4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' - 'lvbkRhdGEuUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeUgAUhxyZWdpc3RlclBhc3N3b3Jk' - 'bGVzc1JlY292ZXJ5EnkKGXBhc3N3b3JkbGVzc19ub3RpZmljYXRpb24YICABKAsyOi5jbGllbn' - 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIY' - 'cGFzc3dvcmRsZXNzTm90aWZpY2F0aW9uEnEKF3JlcXVlc3RfbWVtb3JpZXNfdXBsb2FkGCEgAS' - 'gLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVxdWVzdE1lbW9yaWVzVXBs' - 'b2FkSABSFXJlcXVlc3RNZW1vcmllc1VwbG9hZBJxChdjb25maXJtX21lbW9yaWVzX3VwbG9hZB' - 'giIAEoCzI3LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNvbmZpcm1NZW1vcmll' - 'c1VwbG9hZEgAUhVjb25maXJtTWVtb3JpZXNVcGxvYWQSXwoRZ2V0X21lbW9yaWVzX2xpc3QYIy' - 'ABKAsyMS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc0xpc3RI' - 'AFIPZ2V0TWVtb3JpZXNMaXN0ElwKEGdldF9tZW1vcmllc191cmwYJCABKAsyMC5jbGllbnRfdG' - '9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc1VybEgAUg5nZXRNZW1vcmllc1Vy' - 'bBJiChJnZXRfbWVtb3JpZXNfdXNhZ2UYJSABKAsyMi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2' - 'F0aW9uRGF0YS5HZXRNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2USVQoNZGVsZXRl' - 'X21lbW9yeRgmIAEoCzIuLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlbGV0ZU' - '1lbW9yeUgAUgxkZWxldGVNZW1vcnkaagoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IG' - 'dXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZHkSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYX' - 'RhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASAB' - 'KAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2Vybm' - 'FtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVG' - 'Y20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGhMKEUdldEF2YWlsYW' - 'JsZVBsYW5zGhUKE0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXIS' - 'FwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2' - 'lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgB' - 'IAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZB' - 'gBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJl' - 'a2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYX' - 'R1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRv' - 'a2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2' - 'VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9p' - 'ZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdG' - 'lvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVs' - 'ZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySW' - 'QaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpE' - 'ZXByZWNhdGVkGo4BChxSZWdpc3RlclBhc3N3b3JkTGVzc1JlY292ZXJ5Ei4KEmVuY3J5cHRlZF' - 'NlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVyS2V5EisKDnBpblVubG9ja1Rva2VuGAIg' - 'ASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW5VbmxvY2tUb2tlbhpwChhQYXNzd29yZG' - 'xlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb25J' - 'ZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW5jcnlwdGVkTWVzc2FnZRprChVSZXF1ZX' - 'N0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IEc2l6ZRIjCg1vcmlnaW5hbF9kYXRlGAIg' - 'ASgDUgxvcmlnaW5hbERhdGUSGQoIbWVkaWFfaWQYAyABKAlSB21lZGlhSWQaMgoVQ29uZmlybU' - '1lbW9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dldE1lbW9yaWVz' - 'TGlzdBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdBgCIAEoA1IFbG' - 'ltaXQaSQoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSHAoJdGh1' - 'bWJuYWlsGAIgASgIUgl0aHVtYm5haWwaEgoQR2V0TWVtb3JpZXNVc2FnZRopCgxEZWxldGVNZW' - '1vcnkSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWRCEQoPQXBwbGljYXRpb25EYXRh'); + 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEm' + 'kKE2dldEN1cnJlbnRQbGFuSW5mb3MYCiABKAsyNS5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0' + 'aW9uRGF0YS5HZXRDdXJyZW50UGxhbkluZm9zSABSE2dldEN1cnJlbnRQbGFuSW5mb3MSYwoRZ2' + 'V0QXZhaWxhYmxlUGxhbnMYDCABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0' + 'YS5HZXRBdmFpbGFibGVQbGFuc0gAUhFnZXRBdmFpbGFibGVQbGFucxJUCgxkb3dubG9hZERvbm' + 'UYFCABKAsyLi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5Eb3dubG9hZERvbmVI' + 'AFIMZG93bmxvYWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGBYgASgLMjkuY2xpZW' + '50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5QnlVc2VySWRIAFIX' + 'Z2V0U2lnbmVkUHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2V5GBcgASgLMjQuY2' + 'xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVkUHJlS2V5SABSEnVw' + 'ZGF0ZVNpZ25lZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2xpZW50X3RvX3Nlcn' + 'Zlci5BcHBsaWNhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJl' + 'cG9ydFVzZXIYGSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcn' + 'RVc2VySABSCnJlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9f' + 'c2VydmVyLkFwcGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZR' + 'JRCgtpcGFQdXJjaGFzZRgbIAEoCzItLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRh' + 'LklQQVB1cmNoYXNlSABSC2lwYVB1cmNoYXNlElcKDWlwYUZvcmNlQ2hlY2sYHCABKAsyLy5jbG' + 'llbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5JUEFGb3JjZUNoZWNrSABSDWlwYUZvcmNl' + 'Q2hlY2sSbAoUcmVtb3ZlQWRkaXRpb25hbFVzZXIYEiABKAsyNi5jbGllbnRfdG9fc2VydmVyLk' + 'FwcGxpY2F0aW9uRGF0YS5SZW1vdmVBZGRpdGlvbmFsVXNlckgAUhRyZW1vdmVBZGRpdGlvbmFs' + 'VXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbG' + 'ljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9uYWxVc2VyElkKD3Nl' + 'dF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLl' + 'NldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhKGAQoecmVnaXN0ZXJfcGFzc3dvcmRsZXNz' + 'X3JlY292ZXJ5GB8gASgLMj4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVnaX' + 'N0ZXJQYXNzd29yZExlc3NSZWNvdmVyeUgAUhxyZWdpc3RlclBhc3N3b3JkbGVzc1JlY292ZXJ5' + 'EnkKGXBhc3N3b3JkbGVzc19ub3RpZmljYXRpb24YICABKAsyOi5jbGllbnRfdG9fc2VydmVyLk' + 'FwcGxpY2F0aW9uRGF0YS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIYcGFzc3dvcmRsZXNz' + 'Tm90aWZpY2F0aW9uEnEKF3JlcXVlc3RfbWVtb3JpZXNfdXBsb2FkGCEgASgLMjcuY2xpZW50X3' + 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVxdWVzdE1lbW9yaWVzVXBsb2FkSABSFXJlcXVl' + 'c3RNZW1vcmllc1VwbG9hZBJxChdjb25maXJtX21lbW9yaWVzX3VwbG9hZBgiIAEoCzI3LmNsaW' + 'VudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNvbmZpcm1NZW1vcmllc1VwbG9hZEgAUhVj' + 'b25maXJtTWVtb3JpZXNVcGxvYWQSXwoRZ2V0X21lbW9yaWVzX2xpc3QYIyABKAsyMS5jbGllbn' + 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc0xpc3RIAFIPZ2V0TWVtb3Jp' + 'ZXNMaXN0ElwKEGdldF9tZW1vcmllc191cmwYJCABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFwcG' + 'xpY2F0aW9uRGF0YS5HZXRNZW1vcmllc1VybEgAUg5nZXRNZW1vcmllc1VybBJiChJnZXRfbWVt' + 'b3JpZXNfdXNhZ2UYJSABKAsyMi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZX' + 'RNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2USVQoNZGVsZXRlX21lbW9yeRgmIAEo' + 'CzIuLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlbGV0ZU1lbW9yeUgAUgxkZW' + 'xldGVNZW1vcnkScQoXZGlzYWJsZV9tZW1vcmllc19iYWNrdXAYJyABKAsyNy5jbGllbnRfdG9f' + 'c2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EaXNhYmxlTWVtb3JpZXNCYWNrdXBIAFIVZGlzYWJsZU' + '1lbW9yaWVzQmFja3VwGmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIS' + 'CgRib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNoRGF0YYgBAUIMCg' + 'pfcHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2Vy' + 'bmFtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaNQoUVX' + 'BkYXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2xlRmNtGiYKC0dl' + 'dFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFpbGFibGVQbGFucx' + 'oVChNHZXRDdXJyZW50UGxhbkluZm9zGi8KFFJlbW92ZUFkZGl0aW9uYWxVc2VyEhcKB3VzZXJf' + 'aWQYASABKANSBnVzZXJJZBotChJHZXRQcmVrZXlzQnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1' + 'IGdXNlcklkGjIKF0dldFNpZ25lZFByZUtleUJ5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVz' + 'ZXJJZBqbAQoSVXBkYXRlU2lnbmVkUHJlS2V5EigKEHNpZ25lZF9wcmVrZXlfaWQYASABKANSDn' + 'NpZ25lZFByZWtleUlkEiMKDXNpZ25lZF9wcmVrZXkYAiABKAxSDHNpZ25lZFByZWtleRI2Chdz' + 'aWduZWRfcHJla2V5X3NpZ25hdHVyZRgDIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlGjUKDE' + 'Rvd25sb2FkRG9uZRIlCg5kb3dubG9hZF90b2tlbhgBIAEoDFINZG93bmxvYWRUb2tlbhpOCgpS' + 'ZXBvcnRVc2VyEigKEHJlcG9ydGVkX3VzZXJfaWQYASABKANSDnJlcG9ydGVkVXNlcklkEhYKBn' + 'JlYXNvbhgCIAEoCVIGcmVhc29uGnEKC0lQQVB1cmNoYXNlEh0KCnByb2R1Y3RfaWQYASABKAlS' + 'CXByb2R1Y3RJZBIWCgZzb3VyY2UYAiABKAlSBnNvdXJjZRIrChF2ZXJpZmljYXRpb25fZGF0YR' + 'gDIAEoCVIQdmVyaWZpY2F0aW9uRGF0YRoPCg1JUEFGb3JjZUNoZWNrGg8KDURlbGV0ZUFjY291' + 'bnQaLAoRQWRkQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGjAKDVNldE' + 'xvZ2luVG9rZW4SHwoLbG9naW5fdG9rZW4YASABKAxSCmxvZ2luVG9rZW4ajgEKHFJlZ2lzdGVy' + 'UGFzc3dvcmRMZXNzUmVjb3ZlcnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeX' + 'B0ZWRTZXJ2ZXJLZXkSKwoOcGluVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6I' + 'AQFCEQoPX3BpblVubG9ja1Rva2VuGnAKGFBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3' + 'RpZmljYXRpb25faWQYASABKAlSDm5vdGlmaWNhdGlvbklkEisKEWVuY3J5cHRlZF9tZXNzYWdl' + 'GAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGmsKFVJlcXVlc3RNZW1vcmllc1VwbG9hZBISCgRzaX' + 'plGAEgASgDUgRzaXplEiMKDW9yaWdpbmFsX2RhdGUYAiABKANSDG9yaWdpbmFsRGF0ZRIZCght' + 'ZWRpYV9pZBgDIAEoCVIHbWVkaWFJZBoyChVDb25maXJtTWVtb3JpZXNVcGxvYWQSGQoIbWVkaW' + 'FfaWQYASABKAlSB21lZGlhSWQaSAoPR2V0TWVtb3JpZXNMaXN0Eh8KC29mZnNldF9kYXRlGAEg' + 'ASgDUgpvZmZzZXREYXRlEhQKBWxpbWl0GAIgASgDUgVsaW1pdBpJCg5HZXRNZW1vcmllc1VybB' + 'IZCghtZWRpYV9pZBgBIAEoCVIHbWVkaWFJZBIcCgl0aHVtYm5haWwYAiABKAhSCXRodW1ibmFp' + 'bBoSChBHZXRNZW1vcmllc1VzYWdlGikKDERlbGV0ZU1lbW9yeRIZCghtZWRpYV9pZBgBIAEoCV' + 'IHbWVkaWFJZBoXChVEaXNhYmxlTWVtb3JpZXNCYWNrdXBCEQoPQXBwbGljYXRpb25EYXRhSgQI' + 'CRAKSgQICxAMSgQIDRASSgQIExAU'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/services/api/api.service.dart b/lib/src/services/api/api.service.dart index 60f7e91f..ccec7ac1 100644 --- a/lib/src/services/api/api.service.dart +++ b/lib/src/services/api/api.service.dart @@ -781,6 +781,13 @@ class ApiService { return sendRequestSync(req); } + Future<Result> disableMemoriesBackup() async { + final get = ApplicationData_DisableMemoriesBackup(); + final appData = ApplicationData()..disableMemoriesBackup = get; + final req = createClientToServerFromApplicationData(appData); + return sendRequestSync(req); + } + Future<int?> getUserIdFromUsername(String username) async { final appData = Handshake( getUseridByUsername: Handshake_GetUserIdByUsername(username: username), diff --git a/lib/src/services/memories/memories_cloud.service.dart b/lib/src/services/memories/memories_cloud.service.dart index 4cab3a2a..b90cb86b 100644 --- a/lib/src/services/memories/memories_cloud.service.dart +++ b/lib/src/services/memories/memories_cloud.service.dart @@ -63,10 +63,11 @@ class MemoriesCloudService { Timer? _timer; bool _isProcessing = false; + bool get isProcessing => _isProcessing; final _progressController = - StreamController<MemoriesBackupProgress>.broadcast(); - Stream<MemoriesBackupProgress> get progressStream => + StreamController<MemoriesBackupProgress?>.broadcast(); + Stream<MemoriesBackupProgress?> get progressStream => _progressController.stream; MemoriesBackupProgress? _currentProgress; @@ -97,7 +98,7 @@ class MemoriesCloudService { currentUploadProgress: currentUploadProgress, currentMediaId: currentMediaId, ); - _progressController.add(_currentProgress!); + _progressController.add(_currentProgress); } Future<void> checkUploads() async { @@ -152,6 +153,7 @@ class MemoriesCloudService { } finally { _currentProgress = null; _isProcessing = false; + _progressController.add(null); } } diff --git a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart index c3ffc121..0af5074a 100644 --- a/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/manage_storage.view.dart @@ -1,17 +1,21 @@ import 'dart:async'; +import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/providers/purchases.provider.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart'; @@ -25,6 +29,7 @@ class ManageStorageView extends StatefulWidget { class _ManageStorageViewState extends State<ManageStorageView> { Map<MediaType, int> _stats = {}; server.Response_MemoriesUsage? _memoriesUsage; + int _cloudOnlyCount = 0; @override void initState() { @@ -35,14 +40,109 @@ class _ManageStorageViewState extends State<ManageStorageView> { Future<void> _loadStats() async { final stats = await twonlyDB.mediaFilesDao.getStorageStats(); final memoriesUsage = await apiService.getMemoriesUsage(); + final cloudOnlyCount = await twonlyDB.mediaFilesDao + .getCloudOnlyMemoriesCount(); if (mounted) { setState(() { _stats = stats; _memoriesUsage = memoriesUsage; + _cloudOnlyCount = cloudOnlyCount; }); } } + Future<void> _disableBackup() async { + final confirmed = await showDialog<bool>( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.settingsStorageDisableBackupTitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 16), + Text.rich( + TextSpan( + children: formattedText( + context, + context.lang.settingsStorageDisableBackupBody( + _cloudOnlyCount, + ), + textColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 15), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: MyButton( + variant: MyButtonVariant.secondaryMiddle, + onPressed: () => Navigator.pop(context, false), + child: Text(context.lang.galleryCancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MyButton( + variant: MyButtonVariant.errorMiddle, + onPressed: () => Navigator.pop(context, true), + child: Text(context.lang.settingsStorageDisableBackupBtn), + ), + ), + ], + ), + ], + ), + ), + ), + ); + + if (confirmed == true) { + try { + await apiService.disableMemoriesBackup(); + final allMedias = await (twonlyDB.select( + twonlyDB.mediaFiles, + )..where((t) => t.stored.equals(true))).get(); + for (final media in allMedias) { + final ms = MediaFileService(media); + if (!ms.storedPath.existsSync()) { + ms.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId); + } + } + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + cloudState: Value(CloudState.none), + ), + ); + await UserService.update((u) => u.isCloudBackupEnabled = false); + await _loadStats(); + } catch (e) { + if (mounted) { + showSnackbar(context, e.toString()); + } + } + } + } + @override Widget build(BuildContext context) { final currentPlan = context.watch<PurchasesProvider>().plan; @@ -207,28 +307,84 @@ class _ManageStorageViewState extends State<ManageStorageView> { ), ), ), - StreamBuilder<MemoriesBackupProgress>( + StreamBuilder<MemoriesBackupProgress?>( initialData: memoriesCloudService.currentProgress, stream: memoriesCloudService.progressStream, builder: (context, snapshot) { final progress = snapshot.data; - if (progress == null || progress.totalPending == 0) { - return const SizedBox.shrink(); - } - final percent = - (progress.currentUploaded / progress.totalPending) + - (progress.currentUploadProgress / progress.totalPending); + final isSyncing = + progress != null && progress.totalPending > 0; + final percent = isSyncing + ? ((progress.currentUploaded / progress.totalPending) + + (progress.currentUploadProgress / + progress.totalPending)) + .clamp(0.0, 1.0) + : 0.0; + return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 12), - Text( - 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', - style: const TextStyle(fontSize: 14), + if (isSyncing) ...[ + const SizedBox(height: 12), + Text( + 'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)', + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: percent, + ), + ], + const SizedBox(height: 16), + Align( + child: MyButton( + variant: MyButtonVariant.primaryDense, + onPressed: isSyncing + ? null + : () async { + final memories = await twonlyDB.mediaFilesDao + .getMemoriesToBackup(); + if (memories.isEmpty) { + if (context.mounted) { + showSnackbar( + context, + context + .lang + .settingsStorageSyncUpToDate, + ); + } + } else { + unawaited( + memoriesCloudService.checkUploads(), + ); + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.sync, size: 18), + const SizedBox(width: 8), + Text(context.lang.settingsStorageSyncNow), + ], + ), + ), ), - const SizedBox(height: 6), - LinearProgressIndicator( - value: percent.clamp(0.0, 1.0), + const SizedBox(height: 12), + Align( + child: MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: _disableBackup, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_off_outlined, size: 18), + const SizedBox(width: 8), + Text( + context.lang.settingsStorageDisableBackupAction, + ), + ], + ), + ), ), ], ); diff --git a/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart b/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart index c8f00f6b..62b3ebf2 100644 --- a/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart @@ -60,8 +60,7 @@ class _StorageContentsViewState extends State<StorageContentsView> { switch (_sortOption) { case StorageSortOption.size: storedFiles.sort( - (a, b) => - (b.sizeInBytes ?? 0).compareTo(a.sizeInBytes ?? 0), + (a, b) => (b.sizeInBytes ?? 0).compareTo(a.sizeInBytes ?? 0), ); case StorageSortOption.newest: storedFiles.sort((a, b) => b.createdAt.compareTo(a.createdAt)); @@ -83,7 +82,9 @@ class _StorageContentsViewState extends State<StorageContentsView> { : null, title: isSelecting ? Text( - context.lang.memoriesSelectedCount(_selectedMediaIds.length), + context.lang.memoriesSelectedCount( + _selectedMediaIds.length, + ), style: const TextStyle(fontWeight: FontWeight.bold), ) : Text(context.lang.settingsStorageContents), @@ -98,61 +99,60 @@ class _StorageContentsViewState extends State<StorageContentsView> { body: snapshot.connectionState == ConnectionState.waiting ? const Center(child: CircularProgressIndicator()) : snapshot.hasError - ? Center(child: Text('Error: ${snapshot.error}')) - : storedFiles.isEmpty - ? Center( - child: Text( - context.lang.settingsStorageNoContents, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: Colors.grey), + ? Center(child: Text('Error: ${snapshot.error}')) + : storedFiles.isEmpty + ? Center( + child: Text( + context.lang.settingsStorageNoContents, + style: Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(color: Colors.grey), + ), + ) + : Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + child: Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildChip( + StorageSortOption.size, + context.lang.settingsStorageSortStorage, + ), + const SizedBox(width: 8), + _buildChip( + StorageSortOption.newest, + context.lang.settingsStorageSortNewest, + ), + const SizedBox(width: 8), + _buildChip( + StorageSortOption.oldest, + context.lang.settingsStorageSortOldest, + ), + ], ), - ) - : Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), - child: Align( - alignment: Alignment.centerLeft, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - _buildChip( - StorageSortOption.size, - context.lang.settingsStorageSortStorage, - ), - const SizedBox(width: 8), - _buildChip( - StorageSortOption.newest, - context.lang.settingsStorageSortNewest, - ), - const SizedBox(width: 8), - _buildChip( - StorageSortOption.oldest, - context.lang.settingsStorageSortOldest, - ), - ], - ), - ), - ), - ), - const Divider(height: 1), - Expanded( - child: ListView.builder( - itemCount: storedFiles.length, - itemBuilder: (context, index) { - final file = storedFiles[index]; - return _buildListItem(context, file); - }, - ), - ), - ], ), + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: storedFiles.length, + itemBuilder: (context, index) { + final file = storedFiles[index]; + return _buildListItem(context, file); + }, + ), + ), + ], + ), ); }, ); @@ -183,8 +183,8 @@ class _StorageContentsViewState extends State<StorageContentsView> { final isSelecting = _selectedMediaIds.isNotEmpty; final ms = MediaFileService(file); - final hasStored = file.stored || - (ms.storedPath.existsSync() && ms.storedPath.lengthSync() > 0); + final isLocal = + ms.storedPath.existsSync() && ms.storedPath.lengthSync() > 0; final IconData statusIcon; final String statusText; @@ -197,7 +197,7 @@ class _StorageContentsViewState extends State<StorageContentsView> { statusIcon = Icons.cloud_upload_outlined; statusText = context.lang.settingsStorageLocalOnly; case CloudState.uploaded: - if (hasStored) { + if (isLocal) { statusIcon = Icons.cloud_done_outlined; statusText = context.lang.settingsStorageLocalAndCloud; } else { @@ -260,16 +260,14 @@ class _StorageContentsViewState extends State<StorageContentsView> { final ms = MediaFileService(file); final isVideo = file.type == MediaType.video; - final imgFile = ms.thumbnailPath.existsSync() && - ms.thumbnailPath.lengthSync() > 0 + final imgFile = + ms.thumbnailPath.existsSync() && ms.thumbnailPath.lengthSync() > 0 ? ms.thumbnailPath : ((file.type == MediaType.image || file.type == MediaType.gif) && - ms.storedPath.existsSync() && - ms.storedPath.lengthSync() > 0) + ms.storedPath.existsSync() && + ms.storedPath.lengthSync() > 0) ? ms.storedPath - : (ms.tempPath.existsSync() && - ms.tempPath.lengthSync() > 0 && - !isVideo) + : (ms.tempPath.existsSync() && ms.tempPath.lengthSync() > 0 && !isVideo) ? ms.tempPath : null; @@ -294,16 +292,15 @@ class _StorageContentsViewState extends State<StorageContentsView> { ), ) : file.blurhash != null - ? BlurHash( - hash: file.blurhash!, - optimizationMode: - BlurHashOptimizationMode.approximation, - ) - : const Icon( - Icons.image_outlined, - size: 24, - color: Colors.grey, - ), + ? BlurHash( + hash: file.blurhash!, + optimizationMode: BlurHashOptimizationMode.approximation, + ) + : const Icon( + Icons.image_outlined, + size: 24, + color: Colors.grey, + ), ), if (isVideo) Container( @@ -336,11 +333,12 @@ class _StorageContentsViewState extends State<StorageContentsView> { if (deleteCompletely == null) return; try { - MediaFileService(file).fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); - if (deleteCompletely) { + await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); unawaited(apiService.deleteMemory(file.mediaId)); + MediaFileService(file).fullMediaRemoval(); + } else { + MediaFileService(file).storedPath.deleteSync(); } if (context.mounted) { @@ -360,15 +358,19 @@ class _StorageContentsViewState extends State<StorageContentsView> { } } - Future<void> _batchDelete(BuildContext context, List<MediaFile> allFiles) async { + Future<void> _batchDelete( + BuildContext context, + List<MediaFile> allFiles, + ) async { final selectedCount = _selectedMediaIds.length; if (selectedCount == 0) return; final selectedFiles = allFiles .where((file) => _selectedMediaIds.contains(file.mediaId)) .toList(); - final hasAnyCloudBackup = - selectedFiles.any((file) => file.cloudState == CloudState.uploaded); + final hasAnyCloudBackup = selectedFiles.any( + (file) => file.cloudState == CloudState.uploaded, + ); final deleteCompletely = await showDeleteMemoriesDialog( context: context, @@ -380,11 +382,12 @@ class _StorageContentsViewState extends State<StorageContentsView> { try { for (final file in selectedFiles) { - MediaFileService(file).fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); - if (deleteCompletely) { + await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); unawaited(apiService.deleteMemory(file.mediaId)); + MediaFileService(file).fullMediaRemoval(); + } else { + MediaFileService(file).storedPath.deleteSync(); } } From a55097bfeefddf73b7f8a896d68f10f7cb1ac03f Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Mon, 20 Jul 2026 21:01:46 +0200 Subject: [PATCH 28/29] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7eb06e49..5b89cc2d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.4.1+159 +version: 0.4.1+160 environment: sdk: ^3.11.0 From 5a953226a5321a9b7478936830e4f088e11a5a2e Mon Sep 17 00:00:00 2001 From: otsmr <git@tsmr.eu> Date: Mon, 20 Jul 2026 23:54:33 +0200 Subject: [PATCH 29/29] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5b89cc2d..a2b4e672 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.4.1+160 +version: 0.4.2+161 environment: sdk: ^3.11.0