add joined chip to contact view

This commit is contained in:
otsmr 2026-10-05 22:45:05 +02:00
parent e76eead183
commit ced68167d1
38 changed files with 1539 additions and 243 deletions

View file

@ -1,5 +1,17 @@
# Changelog # Changelog
## 0.6.0
- New: Stickers
- New: Home screen widgets
- New: Stories
- New: Mini games
- New: Custom avatars
- New: Arabic language (Experimental)
- Improve: Message delivery and push notification reliability
- Improve: Many smaller new changes
- Fix: Many small issues
## 0.5.4 ## 0.5.4
- Fix: Recover broken sessions more reliably - Fix: Recover broken sessions more reliably

View file

@ -245,16 +245,21 @@ class FrbUserData {
final PlatformInt64 userId; final PlatformInt64 userId;
final Uint8List username; final Uint8List username;
final Uint8List publicIdentityKey; final Uint8List publicIdentityKey;
final PlatformInt64? joinedAt;
const FrbUserData({ const FrbUserData({
required this.userId, required this.userId,
required this.username, required this.username,
required this.publicIdentityKey, required this.publicIdentityKey,
this.joinedAt,
}); });
@override @override
int get hashCode => int get hashCode =>
userId.hashCode ^ username.hashCode ^ publicIdentityKey.hashCode; userId.hashCode ^
username.hashCode ^
publicIdentityKey.hashCode ^
joinedAt.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -263,7 +268,8 @@ class FrbUserData {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
userId == other.userId && userId == other.userId &&
username == other.username && username == other.username &&
publicIdentityKey == other.publicIdentityKey; publicIdentityKey == other.publicIdentityKey &&
joinedAt == other.joinedAt;
} }
/// Flutter-facing facade for the Rust-owned API runtime. /// Flutter-facing facade for the Rust-owned API runtime.
@ -597,6 +603,9 @@ class RustApi {
static Future<void> purgeWidgetMedia() => static Future<void> purgeWidgetMedia() =>
RustLib.instance.api.crateBridgeApiRustApiPurgeWidgetMedia(); RustLib.instance.api.crateBridgeApiRustApiPurgeWidgetMedia();
static Future<void> refreshContactJoinedDates() =>
RustLib.instance.api.crateBridgeApiRustApiRefreshContactJoinedDates();
/// Republishes the widget manifest. /// Republishes the widget manifest.
/// ///
/// The contact groups a widget offers in its configuration UI are read from /// The contact groups a widget offers in its configuration UI are read from

View file

@ -89,7 +89,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0'; String get codegenVersion => '2.12.0';
@override @override
int get rustContentHash => -1844664049; int get rustContentHash => -1901082359;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@ -417,6 +417,8 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateBridgeApiRustApiPurgeWidgetMedia(); Future<void> crateBridgeApiRustApiPurgeWidgetMedia();
Future<void> crateBridgeApiRustApiRefreshContactJoinedDates();
Future<void> crateBridgeApiRustApiRefreshWidgetManifest(); Future<void> crateBridgeApiRustApiRefreshWidgetManifest();
Future<PlatformInt64> crateBridgeApiRustApiRegister({ Future<PlatformInt64> crateBridgeApiRustApiRegister({
@ -3891,7 +3893,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
@override @override
Future<void> crateBridgeApiRustApiRefreshWidgetManifest() { Future<void> crateBridgeApiRustApiRefreshContactJoinedDates() {
return handler.executeNormal( return handler.executeNormal(
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
@ -3907,6 +3909,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_AnyhowException, decodeErrorData: sse_decode_AnyhowException,
), ),
constMeta: kCrateBridgeApiRustApiRefreshContactJoinedDatesConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateBridgeApiRustApiRefreshContactJoinedDatesConstMeta =>
const TaskConstMeta(
debugName: "rust_api_refresh_contact_joined_dates",
argNames: [],
);
@override
Future<void> crateBridgeApiRustApiRefreshWidgetManifest() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 95,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeApiRustApiRefreshWidgetManifestConstMeta, constMeta: kCrateBridgeApiRustApiRefreshWidgetManifestConstMeta,
argValues: [], argValues: [],
apiImpl: this, apiImpl: this,
@ -3938,7 +3970,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 95, funcId: 96,
port: port_, port: port_,
); );
}, },
@ -3973,7 +4005,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 96, funcId: 97,
port: port_, port: port_,
); );
}, },
@ -4012,7 +4044,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 97, funcId: 98,
port: port_, port: port_,
); );
}, },
@ -4054,7 +4086,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 98, funcId: 99,
port: port_, port: port_,
); );
}, },
@ -4085,7 +4117,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 99, funcId: 100,
port: port_, port: port_,
); );
}, },
@ -4118,7 +4150,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 100, funcId: 101,
port: port_, port: port_,
); );
}, },
@ -4151,7 +4183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 101, funcId: 102,
port: port_, port: port_,
); );
}, },
@ -4186,7 +4218,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 102, funcId: 103,
port: port_, port: port_,
); );
}, },
@ -4219,7 +4251,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 103, funcId: 104,
port: port_, port: port_,
); );
}, },
@ -4252,7 +4284,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 104, funcId: 105,
port: port_, port: port_,
); );
}, },
@ -4285,7 +4317,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 105, funcId: 106,
port: port_, port: port_,
); );
}, },
@ -4322,7 +4354,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 106, funcId: 107,
port: port_, port: port_,
); );
}, },
@ -4352,7 +4384,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 107, funcId: 108,
port: port_, port: port_,
); );
}, },
@ -4382,7 +4414,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 108, funcId: 109,
port: port_, port: port_,
); );
}, },
@ -4412,7 +4444,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 109, funcId: 110,
port: port_, port: port_,
); );
}, },
@ -4445,7 +4477,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 110, funcId: 111,
port: port_, port: port_,
); );
}, },
@ -4476,7 +4508,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 111, funcId: 112,
port: port_, port: port_,
); );
}, },
@ -4509,7 +4541,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 112, funcId: 113,
port: port_, port: port_,
); );
}, },
@ -4555,7 +4587,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 113, funcId: 114,
port: port_, port: port_,
); );
}, },
@ -4611,7 +4643,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 114, funcId: 115,
port: port_, port: port_,
); );
}, },
@ -4661,7 +4693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 115, funcId: 116,
port: port_, port: port_,
); );
}, },
@ -4706,7 +4738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 116, funcId: 117,
port: port_, port: port_,
); );
}, },
@ -4741,7 +4773,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 117, funcId: 118,
port: port_, port: port_,
); );
}, },
@ -4776,7 +4808,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 118, funcId: 119,
port: port_, port: port_,
); );
}, },
@ -4809,7 +4841,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 119, funcId: 120,
port: port_, port: port_,
); );
}, },
@ -4846,7 +4878,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 120, funcId: 121,
port: port_, port: port_,
); );
}, },
@ -4877,7 +4909,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 121, funcId: 122,
port: port_, port: port_,
); );
}, },
@ -4915,7 +4947,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 122, funcId: 123,
port: port_, port: port_,
); );
}, },
@ -4950,7 +4982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 123, funcId: 124,
port: port_, port: port_,
); );
}, },
@ -4989,7 +5021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 124, funcId: 125,
port: port_, port: port_,
); );
}, },
@ -5022,7 +5054,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 125, funcId: 126,
port: port_, port: port_,
); );
}, },
@ -5053,7 +5085,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 126, funcId: 127,
port: port_, port: port_,
); );
}, },
@ -5088,7 +5120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 127, funcId: 128,
port: port_, port: port_,
); );
}, },
@ -5118,7 +5150,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 128, funcId: 129,
port: port_, port: port_,
); );
}, },
@ -5151,7 +5183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 129, funcId: 130,
port: port_, port: port_,
); );
}, },
@ -5186,7 +5218,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 130, funcId: 131,
port: port_, port: port_,
); );
}, },
@ -5219,7 +5251,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 131, funcId: 132,
port: port_, port: port_,
); );
}, },
@ -5250,7 +5282,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 132, funcId: 133,
port: port_, port: port_,
); );
}, },
@ -5287,7 +5319,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 133, funcId: 134,
port: port_, port: port_,
); );
}, },
@ -5339,7 +5371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 134, funcId: 135,
port: port_, port: port_,
); );
}, },
@ -5392,7 +5424,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 135, funcId: 136,
port: port_, port: port_,
); );
}, },
@ -5432,7 +5464,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 136, funcId: 137,
port: port_, port: port_,
); );
}, },
@ -5465,7 +5497,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 137, funcId: 138,
port: port_, port: port_,
); );
}, },
@ -5498,7 +5530,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 138, funcId: 139,
port: port_, port: port_,
); );
}, },
@ -5535,7 +5567,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 139, funcId: 140,
port: port_, port: port_,
); );
}, },
@ -5567,7 +5599,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 140, funcId: 141,
port: port_, port: port_,
); );
}, },
@ -5600,7 +5632,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 141, funcId: 142,
port: port_, port: port_,
); );
}, },
@ -5635,7 +5667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 142, funcId: 143,
port: port_, port: port_,
); );
}, },
@ -5667,7 +5699,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 143, funcId: 144,
port: port_, port: port_,
); );
}, },
@ -5705,7 +5737,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 144, funcId: 145,
port: port_, port: port_,
); );
}, },
@ -5738,7 +5770,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 145, funcId: 146,
port: port_, port: port_,
); );
}, },
@ -5776,7 +5808,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 146, funcId: 147,
port: port_, port: port_,
); );
}, },
@ -5813,7 +5845,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 147, funcId: 148,
port: port_, port: port_,
); );
}, },
@ -5850,7 +5882,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 148, funcId: 149,
port: port_, port: port_,
); );
}, },
@ -5888,7 +5920,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 149, funcId: 150,
port: port_, port: port_,
); );
}, },
@ -5926,7 +5958,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 150, funcId: 151,
port: port_, port: port_,
); );
}, },
@ -5959,7 +5991,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 151, funcId: 152,
port: port_, port: port_,
); );
}, },
@ -5991,7 +6023,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 152, funcId: 153,
port: port_, port: port_,
); );
}, },
@ -6026,7 +6058,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 153, funcId: 154,
port: port_, port: port_,
); );
}, },
@ -6063,7 +6095,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 154, funcId: 155,
port: port_, port: port_,
); );
}, },
@ -6096,7 +6128,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 155, funcId: 156,
port: port_, port: port_,
); );
}, },
@ -6128,7 +6160,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 156, funcId: 157,
port: port_, port: port_,
); );
}, },
@ -6163,7 +6195,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 157, funcId: 158,
port: port_, port: port_,
); );
}, },
@ -6202,7 +6234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 158, funcId: 159,
port: port_, port: port_,
); );
}, },
@ -6239,7 +6271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 159, funcId: 160,
port: port_, port: port_,
); );
}, },
@ -6269,7 +6301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 160, funcId: 161,
port: port_, port: port_,
); );
}, },
@ -6301,7 +6333,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 161, funcId: 162,
port: port_, port: port_,
); );
}, },
@ -6336,7 +6368,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 162, funcId: 163,
port: port_, port: port_,
); );
}, },
@ -6371,7 +6403,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 163, funcId: 164,
port: port_, port: port_,
); );
}, },
@ -6402,7 +6434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 164, funcId: 165,
port: port_, port: port_,
); );
}, },
@ -6440,7 +6472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 165, funcId: 166,
port: port_, port: port_,
); );
}, },
@ -6475,7 +6507,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 166, funcId: 167,
port: port_, port: port_,
); );
}, },
@ -6514,7 +6546,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 167, funcId: 168,
port: port_, port: port_,
); );
}, },
@ -6549,7 +6581,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 168, funcId: 169,
port: port_, port: port_,
); );
}, },
@ -6594,7 +6626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 169, funcId: 170,
port: port_, port: port_,
); );
}, },
@ -6637,7 +6669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 170, funcId: 171,
port: port_, port: port_,
); );
}, },
@ -6669,7 +6701,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 171, funcId: 172,
port: port_, port: port_,
); );
}, },
@ -6704,7 +6736,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 172, funcId: 173,
port: port_, port: port_,
); );
}, },
@ -6739,7 +6771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 173, funcId: 174,
port: port_, port: port_,
); );
}, },
@ -6774,7 +6806,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 174, funcId: 175,
port: port_, port: port_,
); );
}, },
@ -6807,7 +6839,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return pdeCallFfi( return pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 175, funcId: 176,
)!; )!;
}, },
codec: SseCodec( codec: SseCodec(
@ -6847,7 +6879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 176, funcId: 177,
port: port_, port: port_,
); );
}, },
@ -6892,7 +6924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 177, funcId: 178,
port: port_, port: port_,
); );
}, },
@ -6922,7 +6954,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 178, funcId: 179,
port: port_, port: port_,
); );
}, },
@ -6955,7 +6987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 179, funcId: 180,
port: port_, port: port_,
); );
}, },
@ -6990,7 +7022,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 180, funcId: 181,
port: port_, port: port_,
); );
}, },
@ -7029,7 +7061,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return pdeCallFfi( return pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 181, funcId: 182,
)!; )!;
}, },
codec: SseCodec( codec: SseCodec(
@ -7498,12 +7530,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
FrbUserData dco_decode_frb_user_data(dynamic raw) { FrbUserData dco_decode_frb_user_data(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 3) if (arr.length != 4)
throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
return FrbUserData( return FrbUserData(
userId: dco_decode_i_64(arr[0]), userId: dco_decode_i_64(arr[0]),
username: dco_decode_list_prim_u_8_strict(arr[1]), username: dco_decode_list_prim_u_8_strict(arr[1]),
publicIdentityKey: dco_decode_list_prim_u_8_strict(arr[2]), publicIdentityKey: dco_decode_list_prim_u_8_strict(arr[2]),
joinedAt: dco_decode_opt_box_autoadd_i_64(arr[3]),
); );
} }
@ -8128,8 +8161,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
UserConfig dco_decode_user_config(dynamic raw) { UserConfig dco_decode_user_config(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 65) if (arr.length != 66)
throw Exception('unexpected arr length: expect 65 but see ${arr.length}'); throw Exception('unexpected arr length: expect 66 but see ${arr.length}');
return UserConfig( return UserConfig(
userId: dco_decode_i_64(arr[0]), userId: dco_decode_i_64(arr[0]),
username: dco_decode_String(arr[1]), username: dco_decode_String(arr[1]),
@ -8187,20 +8220,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
currentSignedPreKeyIndexStart: dco_decode_i_64(arr[49]), currentSignedPreKeyIndexStart: dco_decode_i_64(arr[49]),
lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[50]), lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[50]),
hideChangeLog: dco_decode_bool(arr[51]), hideChangeLog: dco_decode_bool(arr[51]),
hideMemoriesBackupPromo: dco_decode_bool(arr[52]), lastReleaseNotesVersion: dco_decode_opt_String(arr[52]),
hideWidgetShareExplainer: dco_decode_bool(arr[53]), hideMemoriesBackupPromo: dco_decode_bool(arr[53]),
updateFcmToken: dco_decode_bool(arr[54]), hideWidgetShareExplainer: dco_decode_bool(arr[54]),
canUseLoginTokenForAuth: dco_decode_bool(arr[55]), updateFcmToken: dco_decode_bool(arr[55]),
twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[56]), canUseLoginTokenForAuth: dco_decode_bool(arr[56]),
isBackupEnabled: dco_decode_bool(arr[57]), twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[57]),
isBackupEnabled: dco_decode_bool(arr[58]),
passwordLessRecovery: passwordLessRecovery:
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[58]), dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[59]),
fcmToken: dco_decode_opt_String(arr[59]), fcmToken: dco_decode_opt_String(arr[60]),
lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[60]), lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[61]),
lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[61]), lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[62]),
currentSetupPage: dco_decode_opt_String(arr[62]), currentSetupPage: dco_decode_opt_String(arr[63]),
skipSetupPages: dco_decode_bool(arr[63]), skipSetupPages: dco_decode_bool(arr[64]),
hasZoomed: dco_decode_bool(arr[64]), hasZoomed: dco_decode_bool(arr[65]),
); );
} }
@ -8709,10 +8743,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_userId = sse_decode_i_64(deserializer); var var_userId = sse_decode_i_64(deserializer);
var var_username = sse_decode_list_prim_u_8_strict(deserializer); var var_username = sse_decode_list_prim_u_8_strict(deserializer);
var var_publicIdentityKey = sse_decode_list_prim_u_8_strict(deserializer); var var_publicIdentityKey = sse_decode_list_prim_u_8_strict(deserializer);
var var_joinedAt = sse_decode_opt_box_autoadd_i_64(deserializer);
return FrbUserData( return FrbUserData(
userId: var_userId, userId: var_userId,
username: var_username, username: var_username,
publicIdentityKey: var_publicIdentityKey, publicIdentityKey: var_publicIdentityKey,
joinedAt: var_joinedAt,
); );
} }
@ -9566,6 +9602,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
deserializer, deserializer,
); );
var var_hideChangeLog = sse_decode_bool(deserializer); var var_hideChangeLog = sse_decode_bool(deserializer);
var var_lastReleaseNotesVersion = sse_decode_opt_String(deserializer);
var var_hideMemoriesBackupPromo = sse_decode_bool(deserializer); var var_hideMemoriesBackupPromo = sse_decode_bool(deserializer);
var var_hideWidgetShareExplainer = sse_decode_bool(deserializer); var var_hideWidgetShareExplainer = sse_decode_bool(deserializer);
var var_updateFcmToken = sse_decode_bool(deserializer); var var_updateFcmToken = sse_decode_bool(deserializer);
@ -9638,6 +9675,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
currentSignedPreKeyIndexStart: var_currentSignedPreKeyIndexStart, currentSignedPreKeyIndexStart: var_currentSignedPreKeyIndexStart,
lastChangeLogHash: var_lastChangeLogHash, lastChangeLogHash: var_lastChangeLogHash,
hideChangeLog: var_hideChangeLog, hideChangeLog: var_hideChangeLog,
lastReleaseNotesVersion: var_lastReleaseNotesVersion,
hideMemoriesBackupPromo: var_hideMemoriesBackupPromo, hideMemoriesBackupPromo: var_hideMemoriesBackupPromo,
hideWidgetShareExplainer: var_hideWidgetShareExplainer, hideWidgetShareExplainer: var_hideWidgetShareExplainer,
updateFcmToken: var_updateFcmToken, updateFcmToken: var_updateFcmToken,
@ -10209,6 +10247,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_64(self.userId, serializer); sse_encode_i_64(self.userId, serializer);
sse_encode_list_prim_u_8_strict(self.username, serializer); sse_encode_list_prim_u_8_strict(self.username, serializer);
sse_encode_list_prim_u_8_strict(self.publicIdentityKey, serializer); sse_encode_list_prim_u_8_strict(self.publicIdentityKey, serializer);
sse_encode_opt_box_autoadd_i_64(self.joinedAt, serializer);
} }
@protected @protected
@ -10984,6 +11023,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_64(self.currentSignedPreKeyIndexStart, serializer); sse_encode_i_64(self.currentSignedPreKeyIndexStart, serializer);
sse_encode_opt_list_prim_u_8_strict(self.lastChangeLogHash, serializer); sse_encode_opt_list_prim_u_8_strict(self.lastChangeLogHash, serializer);
sse_encode_bool(self.hideChangeLog, serializer); sse_encode_bool(self.hideChangeLog, serializer);
sse_encode_opt_String(self.lastReleaseNotesVersion, serializer);
sse_encode_bool(self.hideMemoriesBackupPromo, serializer); sse_encode_bool(self.hideMemoriesBackupPromo, serializer);
sse_encode_bool(self.hideWidgetShareExplainer, serializer); sse_encode_bool(self.hideWidgetShareExplainer, serializer);
sse_encode_bool(self.updateFcmToken, serializer); sse_encode_bool(self.updateFcmToken, serializer);

View file

@ -175,6 +175,9 @@ class UserConfig {
PlatformInt64 currentSignedPreKeyIndexStart; PlatformInt64 currentSignedPreKeyIndexStart;
Uint8List? lastChangeLogHash; Uint8List? lastChangeLogHash;
bool hideChangeLog; bool hideChangeLog;
/// Last in-app release note the user dismissed.
String? lastReleaseNotesVersion;
bool hideMemoriesBackupPromo; bool hideMemoriesBackupPromo;
bool hideWidgetShareExplainer; bool hideWidgetShareExplainer;
bool updateFcmToken; bool updateFcmToken;
@ -250,6 +253,7 @@ class UserConfig {
required this.currentSignedPreKeyIndexStart, required this.currentSignedPreKeyIndexStart,
this.lastChangeLogHash, this.lastChangeLogHash,
required this.hideChangeLog, required this.hideChangeLog,
this.lastReleaseNotesVersion,
required this.hideMemoriesBackupPromo, required this.hideMemoriesBackupPromo,
required this.hideWidgetShareExplainer, required this.hideWidgetShareExplainer,
required this.updateFcmToken, required this.updateFcmToken,
@ -319,6 +323,7 @@ class UserConfig {
currentSignedPreKeyIndexStart.hashCode ^ currentSignedPreKeyIndexStart.hashCode ^
lastChangeLogHash.hashCode ^ lastChangeLogHash.hashCode ^
hideChangeLog.hashCode ^ hideChangeLog.hashCode ^
lastReleaseNotesVersion.hashCode ^
hideMemoriesBackupPromo.hashCode ^ hideMemoriesBackupPromo.hashCode ^
hideWidgetShareExplainer.hashCode ^ hideWidgetShareExplainer.hashCode ^
updateFcmToken.hashCode ^ updateFcmToken.hashCode ^
@ -397,6 +402,7 @@ class UserConfig {
other.currentSignedPreKeyIndexStart && other.currentSignedPreKeyIndexStart &&
lastChangeLogHash == other.lastChangeLogHash && lastChangeLogHash == other.lastChangeLogHash &&
hideChangeLog == other.hideChangeLog && hideChangeLog == other.hideChangeLog &&
lastReleaseNotesVersion == other.lastReleaseNotesVersion &&
hideMemoriesBackupPromo == other.hideMemoriesBackupPromo && hideMemoriesBackupPromo == other.hideMemoriesBackupPromo &&
hideWidgetShareExplainer == other.hideWidgetShareExplainer && hideWidgetShareExplainer == other.hideWidgetShareExplainer &&
updateFcmToken == other.updateFcmToken && updateFcmToken == other.updateFcmToken &&

View file

@ -310,6 +310,10 @@ Future<void> postStartupTasks() async {
RustApi.finishStartedMediaUploads(), RustApi.finishStartedMediaUploads(),
'finishStartedMediaUploads', 'finishStartedMediaUploads',
); );
unawaitedRustCall(
RustApi.refreshContactJoinedDates(),
'refreshContactJoinedDates',
);
unawaited( unawaited(
newsService.init().then((_) { newsService.init().then((_) {
final lastDownload = newsService.lastDownloadedAt; final lastDownload = newsService.lastDownloadedAt;

View file

@ -54,6 +54,7 @@ class Contacts extends Table {
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
IntColumn get twonlyScore => integer().nullable()(); IntColumn get twonlyScore => integer().nullable()();
DateTimeColumn get joinedAt => dateTime().nullable()();
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))(); IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
IntColumn get mediaReceivedCounter => IntColumn get mediaReceivedCounter =>

View file

@ -348,6 +348,17 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _joinedAtMeta = const VerificationMeta(
'joinedAt',
);
@override
late final GeneratedColumn<DateTime> joinedAt = GeneratedColumn<DateTime>(
'joined_at',
aliasedName,
true,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const VerificationMeta _mediaSendCounterMeta = const VerificationMeta( static const VerificationMeta _mediaSendCounterMeta = const VerificationMeta(
'mediaSendCounter', 'mediaSendCounter',
); );
@ -400,6 +411,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
widgetSharingAllowed, widgetSharingAllowed,
widgetSharingGranted, widgetSharingGranted,
twonlyScore, twonlyScore,
joinedAt,
mediaSendCounter, mediaSendCounter,
mediaReceivedCounter, mediaReceivedCounter,
]; ];
@ -627,6 +639,12 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
), ),
); );
} }
if (data.containsKey('joined_at')) {
context.handle(
_joinedAtMeta,
joinedAt.isAcceptableOrUnknown(data['joined_at']!, _joinedAtMeta),
);
}
if (data.containsKey('media_send_counter')) { if (data.containsKey('media_send_counter')) {
context.handle( context.handle(
_mediaSendCounterMeta, _mediaSendCounterMeta,
@ -764,6 +782,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}twonly_score'], data['${effectivePrefix}twonly_score'],
), ),
joinedAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}joined_at'],
),
mediaSendCounter: attachedDatabase.typeMapping.read( mediaSendCounter: attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}media_send_counter'], data['${effectivePrefix}media_send_counter'],
@ -814,6 +836,7 @@ class Contact extends DataClass implements Insertable<Contact> {
final bool widgetSharingAllowed; final bool widgetSharingAllowed;
final bool widgetSharingGranted; final bool widgetSharingGranted;
final int? twonlyScore; final int? twonlyScore;
final DateTime? joinedAt;
final int mediaSendCounter; final int mediaSendCounter;
final int mediaReceivedCounter; final int mediaReceivedCounter;
const Contact({ const Contact({
@ -844,6 +867,7 @@ class Contact extends DataClass implements Insertable<Contact> {
required this.widgetSharingAllowed, required this.widgetSharingAllowed,
required this.widgetSharingGranted, required this.widgetSharingGranted,
this.twonlyScore, this.twonlyScore,
this.joinedAt,
required this.mediaSendCounter, required this.mediaSendCounter,
required this.mediaReceivedCounter, required this.mediaReceivedCounter,
}); });
@ -915,6 +939,9 @@ class Contact extends DataClass implements Insertable<Contact> {
if (!nullToAbsent || twonlyScore != null) { if (!nullToAbsent || twonlyScore != null) {
map['twonly_score'] = Variable<int>(twonlyScore); map['twonly_score'] = Variable<int>(twonlyScore);
} }
if (!nullToAbsent || joinedAt != null) {
map['joined_at'] = Variable<DateTime>(joinedAt);
}
map['media_send_counter'] = Variable<int>(mediaSendCounter); map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter); map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map; return map;
@ -977,6 +1004,9 @@ class Contact extends DataClass implements Insertable<Contact> {
twonlyScore: twonlyScore == null && nullToAbsent twonlyScore: twonlyScore == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(twonlyScore), : Value(twonlyScore),
joinedAt: joinedAt == null && nullToAbsent
? const Value.absent()
: Value(joinedAt),
mediaSendCounter: Value(mediaSendCounter), mediaSendCounter: Value(mediaSendCounter),
mediaReceivedCounter: Value(mediaReceivedCounter), mediaReceivedCounter: Value(mediaReceivedCounter),
); );
@ -1045,6 +1075,7 @@ class Contact extends DataClass implements Insertable<Contact> {
json['widgetSharingGranted'], json['widgetSharingGranted'],
), ),
twonlyScore: serializer.fromJson<int?>(json['twonlyScore']), twonlyScore: serializer.fromJson<int?>(json['twonlyScore']),
joinedAt: serializer.fromJson<DateTime?>(json['joinedAt']),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']), mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
mediaReceivedCounter: serializer.fromJson<int>( mediaReceivedCounter: serializer.fromJson<int>(
json['mediaReceivedCounter'], json['mediaReceivedCounter'],
@ -1100,6 +1131,7 @@ class Contact extends DataClass implements Insertable<Contact> {
'widgetSharingAllowed': serializer.toJson<bool>(widgetSharingAllowed), 'widgetSharingAllowed': serializer.toJson<bool>(widgetSharingAllowed),
'widgetSharingGranted': serializer.toJson<bool>(widgetSharingGranted), 'widgetSharingGranted': serializer.toJson<bool>(widgetSharingGranted),
'twonlyScore': serializer.toJson<int?>(twonlyScore), 'twonlyScore': serializer.toJson<int?>(twonlyScore),
'joinedAt': serializer.toJson<DateTime?>(joinedAt),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter), 'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter), 'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter),
}; };
@ -1133,6 +1165,7 @@ class Contact extends DataClass implements Insertable<Contact> {
bool? widgetSharingAllowed, bool? widgetSharingAllowed,
bool? widgetSharingGranted, bool? widgetSharingGranted,
Value<int?> twonlyScore = const Value.absent(), Value<int?> twonlyScore = const Value.absent(),
Value<DateTime?> joinedAt = const Value.absent(),
int? mediaSendCounter, int? mediaSendCounter,
int? mediaReceivedCounter, int? mediaReceivedCounter,
}) => Contact( }) => Contact(
@ -1182,6 +1215,7 @@ class Contact extends DataClass implements Insertable<Contact> {
widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed, widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed,
widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted, widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted,
twonlyScore: twonlyScore.present ? twonlyScore.value : this.twonlyScore, twonlyScore: twonlyScore.present ? twonlyScore.value : this.twonlyScore,
joinedAt: joinedAt.present ? joinedAt.value : this.joinedAt,
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter,
); );
@ -1252,6 +1286,7 @@ class Contact extends DataClass implements Insertable<Contact> {
twonlyScore: data.twonlyScore.present twonlyScore: data.twonlyScore.present
? data.twonlyScore.value ? data.twonlyScore.value
: this.twonlyScore, : this.twonlyScore,
joinedAt: data.joinedAt.present ? data.joinedAt.value : this.joinedAt,
mediaSendCounter: data.mediaSendCounter.present mediaSendCounter: data.mediaSendCounter.present
? data.mediaSendCounter.value ? data.mediaSendCounter.value
: this.mediaSendCounter, : this.mediaSendCounter,
@ -1293,6 +1328,7 @@ class Contact extends DataClass implements Insertable<Contact> {
..write('widgetSharingAllowed: $widgetSharingAllowed, ') ..write('widgetSharingAllowed: $widgetSharingAllowed, ')
..write('widgetSharingGranted: $widgetSharingGranted, ') ..write('widgetSharingGranted: $widgetSharingGranted, ')
..write('twonlyScore: $twonlyScore, ') ..write('twonlyScore: $twonlyScore, ')
..write('joinedAt: $joinedAt, ')
..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaSendCounter: $mediaSendCounter, ')
..write('mediaReceivedCounter: $mediaReceivedCounter') ..write('mediaReceivedCounter: $mediaReceivedCounter')
..write(')')) ..write(')'))
@ -1328,6 +1364,7 @@ class Contact extends DataClass implements Insertable<Contact> {
widgetSharingAllowed, widgetSharingAllowed,
widgetSharingGranted, widgetSharingGranted,
twonlyScore, twonlyScore,
joinedAt,
mediaSendCounter, mediaSendCounter,
mediaReceivedCounter, mediaReceivedCounter,
]); ]);
@ -1376,6 +1413,7 @@ class Contact extends DataClass implements Insertable<Contact> {
other.widgetSharingAllowed == this.widgetSharingAllowed && other.widgetSharingAllowed == this.widgetSharingAllowed &&
other.widgetSharingGranted == this.widgetSharingGranted && other.widgetSharingGranted == this.widgetSharingGranted &&
other.twonlyScore == this.twonlyScore && other.twonlyScore == this.twonlyScore &&
other.joinedAt == this.joinedAt &&
other.mediaSendCounter == this.mediaSendCounter && other.mediaSendCounter == this.mediaSendCounter &&
other.mediaReceivedCounter == this.mediaReceivedCounter); other.mediaReceivedCounter == this.mediaReceivedCounter);
} }
@ -1408,6 +1446,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
final Value<bool> widgetSharingAllowed; final Value<bool> widgetSharingAllowed;
final Value<bool> widgetSharingGranted; final Value<bool> widgetSharingGranted;
final Value<int?> twonlyScore; final Value<int?> twonlyScore;
final Value<DateTime?> joinedAt;
final Value<int> mediaSendCounter; final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter; final Value<int> mediaReceivedCounter;
const ContactsCompanion({ const ContactsCompanion({
@ -1438,6 +1477,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
this.widgetSharingAllowed = const Value.absent(), this.widgetSharingAllowed = const Value.absent(),
this.widgetSharingGranted = const Value.absent(), this.widgetSharingGranted = const Value.absent(),
this.twonlyScore = const Value.absent(), this.twonlyScore = const Value.absent(),
this.joinedAt = const Value.absent(),
this.mediaSendCounter = const Value.absent(), this.mediaSendCounter = const Value.absent(),
this.mediaReceivedCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(),
}); });
@ -1469,6 +1509,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
this.widgetSharingAllowed = const Value.absent(), this.widgetSharingAllowed = const Value.absent(),
this.widgetSharingGranted = const Value.absent(), this.widgetSharingGranted = const Value.absent(),
this.twonlyScore = const Value.absent(), this.twonlyScore = const Value.absent(),
this.joinedAt = const Value.absent(),
this.mediaSendCounter = const Value.absent(), this.mediaSendCounter = const Value.absent(),
this.mediaReceivedCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(),
}) : username = Value(username); }) : username = Value(username);
@ -1500,6 +1541,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Expression<bool>? widgetSharingAllowed, Expression<bool>? widgetSharingAllowed,
Expression<bool>? widgetSharingGranted, Expression<bool>? widgetSharingGranted,
Expression<int>? twonlyScore, Expression<int>? twonlyScore,
Expression<DateTime>? joinedAt,
Expression<int>? mediaSendCounter, Expression<int>? mediaSendCounter,
Expression<int>? mediaReceivedCounter, Expression<int>? mediaReceivedCounter,
}) { }) {
@ -1545,6 +1587,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
if (widgetSharingGranted != null) if (widgetSharingGranted != null)
'widget_sharing_granted': widgetSharingGranted, 'widget_sharing_granted': widgetSharingGranted,
if (twonlyScore != null) 'twonly_score': twonlyScore, if (twonlyScore != null) 'twonly_score': twonlyScore,
if (joinedAt != null) 'joined_at': joinedAt,
if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter,
if (mediaReceivedCounter != null) if (mediaReceivedCounter != null)
'media_received_counter': mediaReceivedCounter, 'media_received_counter': mediaReceivedCounter,
@ -1579,6 +1622,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
Value<bool>? widgetSharingAllowed, Value<bool>? widgetSharingAllowed,
Value<bool>? widgetSharingGranted, Value<bool>? widgetSharingGranted,
Value<int?>? twonlyScore, Value<int?>? twonlyScore,
Value<DateTime?>? joinedAt,
Value<int>? mediaSendCounter, Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter, Value<int>? mediaReceivedCounter,
}) { }) {
@ -1618,6 +1662,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed, widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed,
widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted, widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted,
twonlyScore: twonlyScore ?? this.twonlyScore, twonlyScore: twonlyScore ?? this.twonlyScore,
joinedAt: joinedAt ?? this.joinedAt,
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter,
); );
@ -1735,6 +1780,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
if (twonlyScore.present) { if (twonlyScore.present) {
map['twonly_score'] = Variable<int>(twonlyScore.value); map['twonly_score'] = Variable<int>(twonlyScore.value);
} }
if (joinedAt.present) {
map['joined_at'] = Variable<DateTime>(joinedAt.value);
}
if (mediaSendCounter.present) { if (mediaSendCounter.present) {
map['media_send_counter'] = Variable<int>(mediaSendCounter.value); map['media_send_counter'] = Variable<int>(mediaSendCounter.value);
} }
@ -1776,6 +1824,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
..write('widgetSharingAllowed: $widgetSharingAllowed, ') ..write('widgetSharingAllowed: $widgetSharingAllowed, ')
..write('widgetSharingGranted: $widgetSharingGranted, ') ..write('widgetSharingGranted: $widgetSharingGranted, ')
..write('twonlyScore: $twonlyScore, ') ..write('twonlyScore: $twonlyScore, ')
..write('joinedAt: $joinedAt, ')
..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaSendCounter: $mediaSendCounter, ')
..write('mediaReceivedCounter: $mediaReceivedCounter') ..write('mediaReceivedCounter: $mediaReceivedCounter')
..write(')')) ..write(')'))
@ -15812,6 +15861,7 @@ typedef $$ContactsTableCreateCompanionBuilder =
Value<bool> widgetSharingAllowed, Value<bool> widgetSharingAllowed,
Value<bool> widgetSharingGranted, Value<bool> widgetSharingGranted,
Value<int?> twonlyScore, Value<int?> twonlyScore,
Value<DateTime?> joinedAt,
Value<int> mediaSendCounter, Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter, Value<int> mediaReceivedCounter,
}); });
@ -15844,6 +15894,7 @@ typedef $$ContactsTableUpdateCompanionBuilder =
Value<bool> widgetSharingAllowed, Value<bool> widgetSharingAllowed,
Value<bool> widgetSharingGranted, Value<bool> widgetSharingGranted,
Value<int?> twonlyScore, Value<int?> twonlyScore,
Value<DateTime?> joinedAt,
Value<int> mediaSendCounter, Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter, Value<int> mediaReceivedCounter,
}); });
@ -16268,6 +16319,11 @@ class $$ContactsTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<DateTime> get joinedAt => $composableBuilder(
column: $table.joinedAt,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get mediaSendCounter => $composableBuilder( ColumnFilters<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter, column: $table.mediaSendCounter,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -16733,6 +16789,11 @@ class $$ContactsTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<DateTime> get joinedAt => $composableBuilder(
column: $table.joinedAt,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get mediaSendCounter => $composableBuilder( ColumnOrderings<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter, column: $table.mediaSendCounter,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -16875,6 +16936,9 @@ class $$ContactsTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<DateTime> get joinedAt =>
$composableBuilder(column: $table.joinedAt, builder: (column) => column);
GeneratedColumn<int> get mediaSendCounter => $composableBuilder( GeneratedColumn<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter, column: $table.mediaSendCounter,
builder: (column) => column, builder: (column) => column,
@ -17270,6 +17334,7 @@ class $$ContactsTableTableManager
Value<bool> widgetSharingAllowed = const Value.absent(), Value<bool> widgetSharingAllowed = const Value.absent(),
Value<bool> widgetSharingGranted = const Value.absent(), Value<bool> widgetSharingGranted = const Value.absent(),
Value<int?> twonlyScore = const Value.absent(), Value<int?> twonlyScore = const Value.absent(),
Value<DateTime?> joinedAt = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(), Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(), Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion( }) => ContactsCompanion(
@ -17300,6 +17365,7 @@ class $$ContactsTableTableManager
widgetSharingAllowed: widgetSharingAllowed, widgetSharingAllowed: widgetSharingAllowed,
widgetSharingGranted: widgetSharingGranted, widgetSharingGranted: widgetSharingGranted,
twonlyScore: twonlyScore, twonlyScore: twonlyScore,
joinedAt: joinedAt,
mediaSendCounter: mediaSendCounter, mediaSendCounter: mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter,
), ),
@ -17334,6 +17400,7 @@ class $$ContactsTableTableManager
Value<bool> widgetSharingAllowed = const Value.absent(), Value<bool> widgetSharingAllowed = const Value.absent(),
Value<bool> widgetSharingGranted = const Value.absent(), Value<bool> widgetSharingGranted = const Value.absent(),
Value<int?> twonlyScore = const Value.absent(), Value<int?> twonlyScore = const Value.absent(),
Value<DateTime?> joinedAt = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(), Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(), Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion.insert( }) => ContactsCompanion.insert(
@ -17364,6 +17431,7 @@ class $$ContactsTableTableManager
widgetSharingAllowed: widgetSharingAllowed, widgetSharingAllowed: widgetSharingAllowed,
widgetSharingGranted: widgetSharingGranted, widgetSharingGranted: widgetSharingGranted,
twonlyScore: twonlyScore, twonlyScore: twonlyScore,
joinedAt: joinedAt,
mediaSendCounter: mediaSendCounter, mediaSendCounter: mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter, mediaReceivedCounter: mediaReceivedCounter,
), ),

View file

@ -682,6 +682,30 @@ abstract class AppLocalizations {
/// **'The twonly Score reflects a person\'s activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.'** /// **'The twonly Score reflects a person\'s activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.'**
String get contactTwonlyScoreDescription; String get contactTwonlyScoreDescription;
/// No description provided for @contactJoinedToday.
///
/// In en, this message translates to:
/// **'Joined today'**
String get contactJoinedToday;
/// No description provided for @contactJoinedDaysAgo.
///
/// In en, this message translates to:
/// **'{days, plural, =1{Joined 1 day ago} other{Joined {days} days ago}}'**
String contactJoinedDaysAgo(num days);
/// No description provided for @contactJoinedMonthsAgo.
///
/// In en, this message translates to:
/// **'{months, plural, =1{Joined 1 month ago} other{Joined {months} months ago}}'**
String contactJoinedMonthsAgo(num months);
/// No description provided for @contactJoinedYearsAgo.
///
/// In en, this message translates to:
/// **'{years, plural, =1{Joined 1 year ago} other{Joined {years} years ago}}'**
String contactJoinedYearsAgo(num years);
/// No description provided for @privacyVisibilityNobody. /// No description provided for @privacyVisibilityNobody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -5169,6 +5193,96 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete sticker'** /// **'Delete sticker'**
String get deleteSticker; String get deleteSticker;
/// No description provided for @releaseNotesTitle.
///
/// In en, this message translates to:
/// **'What\'s new'**
String get releaseNotesTitle;
/// No description provided for @releaseNotesIntroduction.
///
/// In en, this message translates to:
/// **'The biggest twonly update yet is here. Here\'s a quick look at what\'s new.'**
String get releaseNotesIntroduction;
/// No description provided for @releaseNotesAlsoImproved.
///
/// In en, this message translates to:
/// **'Also improved'**
String get releaseNotesAlsoImproved;
/// No description provided for @releaseNotesStickers.
///
/// In en, this message translates to:
/// **'Stickers'**
String get releaseNotesStickers;
/// No description provided for @releaseNotesStickersDescription.
///
/// In en, this message translates to:
/// **'Turn your photos into stickers and share them in chats.'**
String get releaseNotesStickersDescription;
/// No description provided for @releaseNotesWidgets.
///
/// In en, this message translates to:
/// **'Widgets'**
String get releaseNotesWidgets;
/// No description provided for @releaseNotesWidgetsDescription.
///
/// In en, this message translates to:
/// **'See fresh photos from your favorite people right on your home screen.'**
String get releaseNotesWidgetsDescription;
/// No description provided for @releaseNotesStories.
///
/// In en, this message translates to:
/// **'Stories'**
String get releaseNotesStories;
/// No description provided for @releaseNotesStoriesDescription.
///
/// In en, this message translates to:
/// **'Share moments with your contacts for 24 hours.'**
String get releaseNotesStoriesDescription;
/// No description provided for @releaseNotesMiniGames.
///
/// In en, this message translates to:
/// **'Mini games in chats & groups'**
String get releaseNotesMiniGames;
/// No description provided for @releaseNotesMiniGamesDescription.
///
/// In en, this message translates to:
/// **'Play small games together, right inside your conversations.'**
String get releaseNotesMiniGamesDescription;
/// No description provided for @releaseNotesReliableMessages.
///
/// In en, this message translates to:
/// **'Better message and push reliability'**
String get releaseNotesReliableMessages;
/// No description provided for @releaseNotesImprovedUi.
///
/// In en, this message translates to:
/// **'Improved interface'**
String get releaseNotesImprovedUi;
/// No description provided for @releaseNotesCustomAvatars.
///
/// In en, this message translates to:
/// **'Custom avatars'**
String get releaseNotesCustomAvatars;
/// No description provided for @releaseNotesArabic.
///
/// In en, this message translates to:
/// **'Arabic language (experimental)'**
String get releaseNotesArabic;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -316,6 +316,51 @@ class AppLocalizationsAr extends AppLocalizations {
String get contactTwonlyScoreDescription => String get contactTwonlyScoreDescription =>
'تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.'; 'تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.';
@override
String get contactJoinedToday => 'انضم اليوم';
@override
String contactJoinedDaysAgo(num days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: 'انضم منذ $days يوم',
many: 'انضم منذ $days يومًا',
few: 'انضم منذ $days أيام',
two: 'انضم منذ يومين',
one: 'انضم منذ يوم واحد',
);
return '$_temp0';
}
@override
String contactJoinedMonthsAgo(num months) {
String _temp0 = intl.Intl.pluralLogic(
months,
locale: localeName,
other: 'انضم منذ $months شهر',
many: 'انضم منذ $months شهرًا',
few: 'انضم منذ $months أشهر',
two: 'انضم منذ شهرين',
one: 'انضم منذ شهر واحد',
);
return '$_temp0';
}
@override
String contactJoinedYearsAgo(num years) {
String _temp0 = intl.Intl.pluralLogic(
years,
locale: localeName,
other: 'انضم منذ $years سنة',
many: 'انضم منذ $years سنة',
few: 'انضم منذ $years سنوات',
two: 'انضم منذ سنتين',
one: 'انضم منذ سنة واحدة',
);
return '$_temp0';
}
@override @override
String get privacyVisibilityNobody => 'لا أحد'; String get privacyVisibilityNobody => 'لا أحد';
@ -3043,4 +3088,54 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get deleteSticker => 'حذف الملصق'; String get deleteSticker => 'حذف الملصق';
@override
String get releaseNotesTitle => 'ما الجديد؟';
@override
String get releaseNotesIntroduction =>
'وصل أكبر تحديث لـ twonly حتى الآن. إليك نظرة سريعة على الجديد.';
@override
String get releaseNotesAlsoImproved => 'تحسينات أخرى';
@override
String get releaseNotesStickers => 'الملصقات';
@override
String get releaseNotesStickersDescription =>
'حوّل صورك إلى ملصقات وشاركها في المحادثات.';
@override
String get releaseNotesWidgets => 'الأدوات';
@override
String get releaseNotesWidgetsDescription =>
'شاهد أحدث صور الأشخاص المفضلين لديك مباشرة على شاشتك الرئيسية.';
@override
String get releaseNotesStories => 'القصص';
@override
String get releaseNotesStoriesDescription =>
'شارك لحظاتك مع جهات اتصالك لمدة 24 ساعة.';
@override
String get releaseNotesMiniGames => 'ألعاب مصغّرة في المحادثات والمجموعات';
@override
String get releaseNotesMiniGamesDescription =>
'العبوا ألعابًا صغيرة معًا داخل محادثاتكم مباشرة.';
@override
String get releaseNotesReliableMessages => 'موثوقية أفضل للرسائل والإشعارات';
@override
String get releaseNotesImprovedUi => 'واجهة مستخدم محسّنة';
@override
String get releaseNotesCustomAvatars => 'صور رمزية مخصّصة';
@override
String get releaseNotesArabic => 'اللغة العربية (تجريبية)';
} }

View file

@ -319,6 +319,42 @@ class AppLocalizationsDe extends AppLocalizations {
String get contactTwonlyScoreDescription => String get contactTwonlyScoreDescription =>
'Der twonly Score spiegelt die Aktivität einer Person auf twonly wider. Er wird aus verschiedenen Faktoren berechnet, zum Beispiel daraus, wie viele Fotos und Videos sie sendet und empfängt.'; 'Der twonly Score spiegelt die Aktivität einer Person auf twonly wider. Er wird aus verschiedenen Faktoren berechnet, zum Beispiel daraus, wie viele Fotos und Videos sie sendet und empfängt.';
@override
String get contactJoinedToday => 'Heute beigetreten';
@override
String contactJoinedDaysAgo(num days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: 'Vor $days Tagen beigetreten',
one: 'Vor 1 Tag beigetreten',
);
return '$_temp0';
}
@override
String contactJoinedMonthsAgo(num months) {
String _temp0 = intl.Intl.pluralLogic(
months,
locale: localeName,
other: 'Vor $months Monaten beigetreten',
one: 'Vor 1 Monat beigetreten',
);
return '$_temp0';
}
@override
String contactJoinedYearsAgo(num years) {
String _temp0 = intl.Intl.pluralLogic(
years,
locale: localeName,
other: 'Vor $years Jahren beigetreten',
one: 'Vor 1 Jahr beigetreten',
);
return '$_temp0';
}
@override @override
String get privacyVisibilityNobody => 'Niemand'; String get privacyVisibilityNobody => 'Niemand';
@ -3045,4 +3081,55 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteSticker => 'Sticker löschen'; String get deleteSticker => 'Sticker löschen';
@override
String get releaseNotesTitle => 'Was ist neu?';
@override
String get releaseNotesIntroduction =>
'Das bisher größte twonly-Update ist da. Hier ist ein kurzer Überblick.';
@override
String get releaseNotesAlsoImproved => 'Außerdem verbessert';
@override
String get releaseNotesStickers => 'Sticker';
@override
String get releaseNotesStickersDescription =>
'Erstelle Sticker aus deinen Fotos und teile sie in Chats.';
@override
String get releaseNotesWidgets => 'Widgets';
@override
String get releaseNotesWidgetsDescription =>
'Sieh neue Fotos deiner Lieblingsmenschen direkt auf dem Homescreen.';
@override
String get releaseNotesStories => 'Storys';
@override
String get releaseNotesStoriesDescription =>
'Teile Momente für 24 Stunden mit deinen Kontakten.';
@override
String get releaseNotesMiniGames => 'Minispiele in Chats & Gruppen';
@override
String get releaseNotesMiniGamesDescription =>
'Spielt kleine Spiele gemeinsam direkt in euren Unterhaltungen.';
@override
String get releaseNotesReliableMessages =>
'Zuverlässigere Nachrichten und Push-Benachrichtigungen';
@override
String get releaseNotesImprovedUi => 'Verbesserte Benutzeroberfläche';
@override
String get releaseNotesCustomAvatars => 'Individuelle Avatare';
@override
String get releaseNotesArabic => 'Arabische Sprache (experimentell)';
} }

View file

@ -316,6 +316,42 @@ class AppLocalizationsEn extends AppLocalizations {
String get contactTwonlyScoreDescription => String get contactTwonlyScoreDescription =>
'The twonly Score reflects a person\'s activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.'; 'The twonly Score reflects a person\'s activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.';
@override
String get contactJoinedToday => 'Joined today';
@override
String contactJoinedDaysAgo(num days) {
String _temp0 = intl.Intl.pluralLogic(
days,
locale: localeName,
other: 'Joined $days days ago',
one: 'Joined 1 day ago',
);
return '$_temp0';
}
@override
String contactJoinedMonthsAgo(num months) {
String _temp0 = intl.Intl.pluralLogic(
months,
locale: localeName,
other: 'Joined $months months ago',
one: 'Joined 1 month ago',
);
return '$_temp0';
}
@override
String contactJoinedYearsAgo(num years) {
String _temp0 = intl.Intl.pluralLogic(
years,
locale: localeName,
other: 'Joined $years years ago',
one: 'Joined 1 year ago',
);
return '$_temp0';
}
@override @override
String get privacyVisibilityNobody => 'Nobody'; String get privacyVisibilityNobody => 'Nobody';
@ -3014,4 +3050,55 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get deleteSticker => 'Delete sticker'; String get deleteSticker => 'Delete sticker';
@override
String get releaseNotesTitle => 'What\'s new';
@override
String get releaseNotesIntroduction =>
'The biggest twonly update yet is here. Here\'s a quick look at what\'s new.';
@override
String get releaseNotesAlsoImproved => 'Also improved';
@override
String get releaseNotesStickers => 'Stickers';
@override
String get releaseNotesStickersDescription =>
'Turn your photos into stickers and share them in chats.';
@override
String get releaseNotesWidgets => 'Widgets';
@override
String get releaseNotesWidgetsDescription =>
'See fresh photos from your favorite people right on your home screen.';
@override
String get releaseNotesStories => 'Stories';
@override
String get releaseNotesStoriesDescription =>
'Share moments with your contacts for 24 hours.';
@override
String get releaseNotesMiniGames => 'Mini games in chats & groups';
@override
String get releaseNotesMiniGamesDescription =>
'Play small games together, right inside your conversations.';
@override
String get releaseNotesReliableMessages =>
'Better message and push reliability';
@override
String get releaseNotesImprovedUi => 'Improved interface';
@override
String get releaseNotesCustomAvatars => 'Custom avatars';
@override
String get releaseNotesArabic => 'Arabic language (experimental)';
} }

View file

@ -97,6 +97,10 @@
"publicProfilePrivacyDescription": "اختر من يمكنه رؤية معلومات ملفك الشخصي.", "publicProfilePrivacyDescription": "اختر من يمكنه رؤية معلومات ملفك الشخصي.",
"publicProfileTwonlyScore": "نقاط twonly", "publicProfileTwonlyScore": "نقاط twonly",
"contactTwonlyScoreDescription": "تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.", "contactTwonlyScoreDescription": "تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.",
"contactJoinedToday": "انضم اليوم",
"contactJoinedDaysAgo": "{days, plural, =1{انضم منذ يوم واحد} =2{انضم منذ يومين} few{انضم منذ {days} أيام} many{انضم منذ {days} يومًا} other{انضم منذ {days} يوم}}",
"contactJoinedMonthsAgo": "{months, plural, =1{انضم منذ شهر واحد} =2{انضم منذ شهرين} few{انضم منذ {months} أشهر} many{انضم منذ {months} شهرًا} other{انضم منذ {months} شهر}}",
"contactJoinedYearsAgo": "{years, plural, =1{انضم منذ سنة واحدة} =2{انضم منذ سنتين} few{انضم منذ {years} سنوات} many{انضم منذ {years} سنة} other{انضم منذ {years} سنة}}",
"privacyVisibilityNobody": "لا أحد", "privacyVisibilityNobody": "لا أحد",
"privacyVisibilityOnlyContacts": "جهات الاتصال فقط", "privacyVisibilityOnlyContacts": "جهات الاتصال فقط",
"privacyVisibilityEveryone": "الجميع", "privacyVisibilityEveryone": "الجميع",
@ -865,5 +869,20 @@
"notificationSticker": "أرسل ملصقًا{inGroup}.", "notificationSticker": "أرسل ملصقًا{inGroup}.",
"storeSticker": "حفظ الملصق", "storeSticker": "حفظ الملصق",
"removeSticker": "إزالة من المجموعة", "removeSticker": "إزالة من المجموعة",
"deleteSticker": "حذف الملصق" "deleteSticker": "حذف الملصق",
"releaseNotesTitle": "ما الجديد؟",
"releaseNotesIntroduction": "وصل أكبر تحديث لـ twonly حتى الآن. إليك نظرة سريعة على الجديد.",
"releaseNotesAlsoImproved": "تحسينات أخرى",
"releaseNotesStickers": "الملصقات",
"releaseNotesStickersDescription": "حوّل صورك إلى ملصقات وشاركها في المحادثات.",
"releaseNotesWidgets": "الأدوات",
"releaseNotesWidgetsDescription": "شاهد أحدث صور الأشخاص المفضلين لديك مباشرة على شاشتك الرئيسية.",
"releaseNotesStories": "القصص",
"releaseNotesStoriesDescription": "شارك لحظاتك مع جهات اتصالك لمدة 24 ساعة.",
"releaseNotesMiniGames": "ألعاب مصغّرة في المحادثات والمجموعات",
"releaseNotesMiniGamesDescription": "العبوا ألعابًا صغيرة معًا داخل محادثاتكم مباشرة.",
"releaseNotesReliableMessages": "موثوقية أفضل للرسائل والإشعارات",
"releaseNotesImprovedUi": "واجهة مستخدم محسّنة",
"releaseNotesCustomAvatars": "صور رمزية مخصّصة",
"releaseNotesArabic": "اللغة العربية (تجريبية)"
} }

View file

@ -106,6 +106,10 @@
"publicProfilePrivacyDescription": "Wähle aus, wer Informationen über dein Profil sehen kann.", "publicProfilePrivacyDescription": "Wähle aus, wer Informationen über dein Profil sehen kann.",
"publicProfileTwonlyScore": "twonly Score", "publicProfileTwonlyScore": "twonly Score",
"contactTwonlyScoreDescription": "Der twonly Score spiegelt die Aktivität einer Person auf twonly wider. Er wird aus verschiedenen Faktoren berechnet, zum Beispiel daraus, wie viele Fotos und Videos sie sendet und empfängt.", "contactTwonlyScoreDescription": "Der twonly Score spiegelt die Aktivität einer Person auf twonly wider. Er wird aus verschiedenen Faktoren berechnet, zum Beispiel daraus, wie viele Fotos und Videos sie sendet und empfängt.",
"contactJoinedToday": "Heute beigetreten",
"contactJoinedDaysAgo": "{days, plural, =1{Vor 1 Tag beigetreten} other{Vor {days} Tagen beigetreten}}",
"contactJoinedMonthsAgo": "{months, plural, =1{Vor 1 Monat beigetreten} other{Vor {months} Monaten beigetreten}}",
"contactJoinedYearsAgo": "{years, plural, =1{Vor 1 Jahr beigetreten} other{Vor {years} Jahren beigetreten}}",
"privacyVisibilityNobody": "Niemand", "privacyVisibilityNobody": "Niemand",
"privacyVisibilityOnlyContacts": "Nur Kontakte", "privacyVisibilityOnlyContacts": "Nur Kontakte",
"privacyVisibilityEveryone": "Alle", "privacyVisibilityEveryone": "Alle",
@ -1178,5 +1182,20 @@
"notificationSticker": "hat einen Sticker gesendet{inGroup}.", "notificationSticker": "hat einen Sticker gesendet{inGroup}.",
"storeSticker": "Sticker speichern", "storeSticker": "Sticker speichern",
"removeSticker": "Aus Sammlung entfernen", "removeSticker": "Aus Sammlung entfernen",
"deleteSticker": "Sticker löschen" "deleteSticker": "Sticker löschen",
"releaseNotesTitle": "Was ist neu?",
"releaseNotesIntroduction": "Das bisher größte twonly-Update ist da. Hier ist ein kurzer Überblick.",
"releaseNotesAlsoImproved": "Außerdem verbessert",
"releaseNotesStickers": "Sticker",
"releaseNotesStickersDescription": "Erstelle Sticker aus deinen Fotos und teile sie in Chats.",
"releaseNotesWidgets": "Widgets",
"releaseNotesWidgetsDescription": "Sieh neue Fotos deiner Lieblingsmenschen direkt auf dem Homescreen.",
"releaseNotesStories": "Storys",
"releaseNotesStoriesDescription": "Teile Momente für 24 Stunden mit deinen Kontakten.",
"releaseNotesMiniGames": "Minispiele in Chats & Gruppen",
"releaseNotesMiniGamesDescription": "Spielt kleine Spiele gemeinsam direkt in euren Unterhaltungen.",
"releaseNotesReliableMessages": "Zuverlässigere Nachrichten und Push-Benachrichtigungen",
"releaseNotesImprovedUi": "Verbesserte Benutzeroberfläche",
"releaseNotesCustomAvatars": "Individuelle Avatare",
"releaseNotesArabic": "Arabische Sprache (experimentell)"
} }

View file

@ -102,6 +102,10 @@
"publicProfilePrivacyDescription": "Choose who can see information about your profile.", "publicProfilePrivacyDescription": "Choose who can see information about your profile.",
"publicProfileTwonlyScore": "twonly Score", "publicProfileTwonlyScore": "twonly Score",
"contactTwonlyScoreDescription": "The twonly Score reflects a person's activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.", "contactTwonlyScoreDescription": "The twonly Score reflects a person's activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.",
"contactJoinedToday": "Joined today",
"contactJoinedDaysAgo": "{days, plural, =1{Joined 1 day ago} other{Joined {days} days ago}}",
"contactJoinedMonthsAgo": "{months, plural, =1{Joined 1 month ago} other{Joined {months} months ago}}",
"contactJoinedYearsAgo": "{years, plural, =1{Joined 1 year ago} other{Joined {years} years ago}}",
"privacyVisibilityNobody": "Nobody", "privacyVisibilityNobody": "Nobody",
"privacyVisibilityOnlyContacts": "Only Contacts", "privacyVisibilityOnlyContacts": "Only Contacts",
"privacyVisibilityEveryone": "Everyone", "privacyVisibilityEveryone": "Everyone",
@ -1188,5 +1192,20 @@
"notificationSticker": "sent a sticker{inGroup}.", "notificationSticker": "sent a sticker{inGroup}.",
"storeSticker": "Save sticker", "storeSticker": "Save sticker",
"removeSticker": "Remove from collection", "removeSticker": "Remove from collection",
"deleteSticker": "Delete sticker" "deleteSticker": "Delete sticker",
"releaseNotesTitle": "What's new",
"releaseNotesIntroduction": "The biggest twonly update yet is here. Here's a quick look at what's new.",
"releaseNotesAlsoImproved": "Also improved",
"releaseNotesStickers": "Stickers",
"releaseNotesStickersDescription": "Turn your photos into stickers and share them in chats.",
"releaseNotesWidgets": "Widgets",
"releaseNotesWidgetsDescription": "See fresh photos from your favorite people right on your home screen.",
"releaseNotesStories": "Stories",
"releaseNotesStoriesDescription": "Share moments with your contacts for 24 hours.",
"releaseNotesMiniGames": "Mini games in chats & groups",
"releaseNotesMiniGamesDescription": "Play small games together, right inside your conversations.",
"releaseNotesReliableMessages": "Better message and push reliability",
"releaseNotesImprovedUi": "Improved interface",
"releaseNotesCustomAvatars": "Custom avatars",
"releaseNotesArabic": "Arabic language (experimental)"
} }

View file

@ -68,6 +68,10 @@ class ApiService {
rust_api.RustApi.reuploadPendingMedia(), rust_api.RustApi.reuploadPendingMedia(),
'reuploadPendingMedia', 'reuploadPendingMedia',
); );
unawaitedRustCall(
rust_api.RustApi.refreshContactJoinedDates(),
'refreshContactJoinedDates',
);
twonlyDB.markUpdated(); twonlyDB.markUpdated();
// resetUserDiscoveryRequestUpdates(); // resetUserDiscoveryRequestUpdates();

View file

@ -99,6 +99,22 @@ String formatDuration(BuildContext context, int seconds) {
} }
} }
String formatJoinedAt(BuildContext context, DateTime joinedAt) {
final elapsed = clock.now().toUtc().difference(joinedAt.toUtc());
final days = max(0, elapsed.inDays);
if (days == 0) {
return context.lang.contactJoinedToday;
}
if (days < 30) {
return context.lang.contactJoinedDaysAgo(days);
}
if (days < 365) {
return context.lang.contactJoinedMonthsAgo(days ~/ 30);
}
return context.lang.contactJoinedYearsAgo(days ~/ 365);
}
Future<bool> authenticateUser( Future<bool> authenticateUser(
String localizedReason, { String localizedReason, {
bool force = true, bool force = true,

View file

@ -0,0 +1,312 @@
import 'package:flutter/material.dart';
import 'package:twonly/locator.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';
@immutable
class ReleaseNoteFeature {
const ReleaseNoteFeature({
required this.icon,
required this.title,
required this.description,
});
final Widget icon;
final String title;
final String description;
}
@immutable
class ReleaseNotesData {
const ReleaseNotesData({
required this.version,
required this.title,
required this.introduction,
required this.additionalChangesTitle,
required this.closeLabel,
required this.features,
required this.additionalChanges,
});
final String version;
final String title;
final String introduction;
final String additionalChangesTitle;
final String closeLabel;
final List<ReleaseNoteFeature> features;
final List<String> additionalChanges;
}
/// Coordinates the once-per-version behavior for any release note.
class ReleaseNotesPresenter {
ReleaseNotesPresenter._();
static final Set<String> _showingVersions = {};
static final Set<String> _shownThisSession = {};
static Future<void> showIfNeeded(
BuildContext context,
ReleaseNotesData releaseNotes,
) async {
final version = releaseNotes.version;
if (_showingVersions.contains(version) ||
_shownThisSession.contains(version) ||
userService.currentUser.lastReleaseNotesVersion == version) {
return;
}
_showingVersions.add(version);
try {
await showReleaseNotesBottomSheet(context, releaseNotes);
_shownThisSession.add(version);
await UserService.update(
(user) => user.lastReleaseNotesVersion = version,
);
} finally {
_showingVersions.remove(version);
}
}
}
Future<void> showReleaseNotesBottomSheet(
BuildContext context,
ReleaseNotesData releaseNotes,
) {
return showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => FractionallySizedBox(
heightFactor: 0.8,
child: ReleaseNotesBottomSheet(releaseNotes: releaseNotes),
),
);
}
class ReleaseNotesBottomSheet extends StatelessWidget {
const ReleaseNotesBottomSheet({
required this.releaseNotes,
super.key,
});
final ReleaseNotesData releaseNotes;
@override
Widget build(BuildContext context) {
return Material(
color: context.color.surface,
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
child: SafeArea(
top: false,
child: Column(
children: [
const _DragHandle(),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
children: [
_Header(releaseNotes: releaseNotes),
const SizedBox(height: 24),
...releaseNotes.features.map(
(feature) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _FeatureCard(feature: feature),
),
),
if (releaseNotes.additionalChanges.isNotEmpty) ...[
const SizedBox(height: 10),
Text(
releaseNotes.additionalChangesTitle,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: context.color.onSurfaceVariant,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 10),
...releaseNotes.additionalChanges.map(
(change) => _AdditionalChange(text: change),
),
],
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 12),
child: MyButton(
onPressed: () => Navigator.pop(context),
child: Text(releaseNotes.closeLabel),
),
),
],
),
),
);
}
}
class _DragHandle extends StatelessWidget {
const _DragHandle();
@override
Widget build(BuildContext context) {
return Container(
width: 42,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: context.color.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
);
}
}
class _Header extends StatelessWidget {
const _Header({required this.releaseNotes});
final ReleaseNotesData releaseNotes;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: context.color.primaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Text(
'v${releaseNotes.version}',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: context.color.onPrimaryContainer,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 12),
Text(
releaseNotes.title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
Text(
releaseNotes.introduction,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: context.color.onSurfaceVariant,
height: 1.35,
),
),
],
);
}
}
class _FeatureCard extends StatelessWidget {
const _FeatureCard({required this.feature});
final ReleaseNoteFeature feature;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: context.color.surfaceContainerLow,
borderRadius: BorderRadius.circular(18),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: context.color.primaryContainer,
borderRadius: BorderRadius.circular(14),
),
alignment: Alignment.center,
child: IconTheme(
data: IconThemeData(
size: 20,
color: context.color.onPrimaryContainer,
),
child: feature.icon,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
feature.title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 3),
Text(
feature.description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: context.color.onSurfaceVariant,
height: 1.35,
),
),
],
),
),
],
),
),
);
}
}
class _AdditionalChange extends StatelessWidget {
const _AdditionalChange({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: context.color.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.check_rounded,
size: 15,
color: context.color.onPrimaryContainer,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
);
}
}

View file

@ -0,0 +1,43 @@
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/components/release_notes.comp.dart';
ReleaseNotesData releaseNotes060(BuildContext context) {
final lang = context.lang;
return ReleaseNotesData(
version: '0.6.0',
title: lang.releaseNotesTitle,
introduction: lang.releaseNotesIntroduction,
additionalChangesTitle: lang.releaseNotesAlsoImproved,
closeLabel: lang.close,
features: [
ReleaseNoteFeature(
icon: const FaIcon(FontAwesomeIcons.noteSticky),
title: lang.releaseNotesStickers,
description: lang.releaseNotesStickersDescription,
),
ReleaseNoteFeature(
icon: const FaIcon(FontAwesomeIcons.tableCellsLarge),
title: lang.releaseNotesWidgets,
description: lang.releaseNotesWidgetsDescription,
),
ReleaseNoteFeature(
icon: const FaIcon(FontAwesomeIcons.circlePlay),
title: lang.releaseNotesStories,
description: lang.releaseNotesStoriesDescription,
),
ReleaseNoteFeature(
icon: const FaIcon(FontAwesomeIcons.gamepad),
title: lang.releaseNotesMiniGames,
description: lang.releaseNotesMiniGamesDescription,
),
],
additionalChanges: [
lang.releaseNotesReliableMessages,
lang.releaseNotesImprovedUi,
lang.releaseNotesCustomAvatars,
lang.releaseNotesArabic,
],
);
}

View file

@ -136,6 +136,14 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
requested: const Value(false), requested: const Value(false),
blocked: const Value(false), blocked: const Value(false),
deletedByUser: const Value(false), deletedByUser: const Value(false),
joinedAt: userdata.joinedAt == null
? const Value.absent()
: Value(
DateTime.fromMillisecondsSinceEpoch(
userdata.joinedAt! * Duration.millisecondsPerSecond,
isUtc: true,
),
),
), ),
); );
await RustApi.tryRequestContactById( await RustApi.tryRequestContactById(

View file

@ -170,6 +170,14 @@ class _ContactRowState extends State<_ContactRow> {
requested: const Value(false), requested: const Value(false),
blocked: const Value(false), blocked: const Value(false),
deletedByUser: const Value(false), deletedByUser: const Value(false),
joinedAt: userdata.joinedAt == null
? const Value.absent()
: Value(
DateTime.fromMillisecondsSinceEpoch(
userdata.joinedAt! * Duration.millisecondsPerSecond,
isUtc: true,
),
),
), ),
); );

View file

@ -158,6 +158,14 @@ class _SearchUsernameView extends State<AddNewUserView> {
requested: const Value(false), requested: const Value(false),
blocked: const Value(false), blocked: const Value(false),
deletedByUser: const Value(false), deletedByUser: const Value(false),
joinedAt: userdata.joinedAt == null
? const Value.absent()
: Value(
DateTime.fromMillisecondsSinceEpoch(
userdata.joinedAt! * Duration.millisecondsPerSecond,
isUtc: true,
),
),
), ),
); );

View file

@ -68,6 +68,14 @@ class FriendSuggestionsComp extends StatelessWidget {
requested: const Value(false), requested: const Value(false),
blocked: const Value(false), blocked: const Value(false),
deletedByUser: const Value(false), deletedByUser: const Value(false),
joinedAt: userdata.joinedAt == null
? const Value.absent()
: Value(
DateTime.fromMillisecondsSinceEpoch(
userdata.joinedAt! * Duration.millisecondsPerSecond,
isUtc: true,
),
),
), ),
); );

View file

@ -226,36 +226,64 @@ class _ContactViewState extends State<ContactView> {
), ),
if (getContactDisplayName(contact) != contact.username) if (getContactDisplayName(contact) != contact.username)
Center(child: Text('(${contact.username})')), Center(child: Text('(${contact.username})')),
if (contact.twonlyScore != null) if (contact.twonlyScore != null ||
(contact.joinedAt != null && !contact.accountDeleted))
Padding( Padding(
padding: const EdgeInsets.only(top: 10), padding: const EdgeInsets.only(top: 10),
child: Center( child: Center(
child: IntrinsicWidth( child: Wrap(
child: MyChip( alignment: WrapAlignment.center,
label: Row( spacing: 8,
mainAxisSize: MainAxisSize.min, runSpacing: 8,
children: [ children: [
const FaIcon(FontAwesomeIcons.trophy, size: 14), if (contact.twonlyScore != null)
const SizedBox(width: 7), IntrinsicWidth(
Text('${contact.twonlyScore}'), child: MyChip(
], label: Row(
), mainAxisSize: MainAxisSize.min,
onTap: () => showDialog<void>( children: [
context: context, const FaIcon(FontAwesomeIcons.trophy, size: 14),
builder: (context) => AlertDialog( const SizedBox(width: 7),
title: Text(context.lang.publicProfileTwonlyScore), Text('${contact.twonlyScore}'),
content: Text( ],
context.lang.contactTwonlyScoreDescription,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.lang.ok),
), ),
], onTap: () => showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(
context.lang.publicProfileTwonlyScore,
),
content: Text(
context.lang.contactTwonlyScoreDescription,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.lang.ok),
),
],
),
),
),
), ),
), if (contact.joinedAt != null && !contact.accountDeleted)
), IntrinsicWidth(
child: MyChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FaIcon(
FontAwesomeIcons.calendarDay,
size: 14,
),
const SizedBox(width: 7),
Text(formatJoinedAt(context, contact.joinedAt!)),
],
),
onTap: null,
),
),
],
), ),
), ),
), ),

View file

@ -17,6 +17,8 @@ import 'package:twonly/src/services/notifications/native.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/release_notes.comp.dart';
import 'package:twonly/src/visual/release_notes/release_notes_0_6_0.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.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
@ -45,6 +47,8 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
/// Whether [_openEditor] has the editor open over this view. /// Whether [_openEditor] has the editor open over this view.
bool _editorOpen = false; bool _editorOpen = false;
bool _startPreloading = false; bool _startPreloading = false;
bool _startupChecksComplete = false;
bool _releaseNotesCheckScheduled = false;
final MainCameraController _mainCameraController = MainCameraController(); final MainCameraController _mainCameraController = MainCameraController();
late final PageController _homeViewPageController; late final PageController _homeViewPageController;
@ -67,6 +71,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
routerProvider.routerDelegate.addListener(_onRouteChanged);
var initialPage = widget.initialPage; var initialPage = widget.initialPage;
if (HomeViewState.pendingSharedLink != null) { if (HomeViewState.pendingSharedLink != null) {
initialPage = 1; initialPage = 1;
@ -97,6 +102,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
if (index != 1) { if (index != 1) {
unawaited(_mainCameraController.closeCamera()); unawaited(_mainCameraController.closeCamera());
} }
_scheduleReleaseNotesIfNeeded();
}); });
_selectNotificationSub = selectNotificationStream.stream.listen(( _selectNotificationSub = selectNotificationStream.stream.listen((
@ -179,7 +185,12 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
}); });
} }
unawaited(_initAsync()); unawaited(
_initAsync().whenComplete(() {
_startupChecksComplete = true;
_scheduleReleaseNotesIfNeeded();
}),
);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_mainCameraController.sharedLinkForPreview == null && if (_mainCameraController.sharedLinkForPreview == null &&
@ -293,6 +304,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
routerProvider.routerDelegate.removeListener(_onRouteChanged);
_onMessageOpenedAppSub?.cancel(); _onMessageOpenedAppSub?.cancel();
_homeViewPageIndexSub?.cancel(); _homeViewPageIndexSub?.cancel();
_selectNotificationSub?.cancel(); _selectNotificationSub?.cancel();
@ -310,10 +322,29 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
return ModalRoute.of(context)?.isCurrent ?? false; return ModalRoute.of(context)?.isCurrent ?? false;
} }
void _onRouteChanged() => _scheduleReleaseNotesIfNeeded();
void _scheduleReleaseNotesIfNeeded() {
if (!_startupChecksComplete ||
_releaseNotesCheckScheduled ||
_activePageIdx != 0) {
return;
}
_releaseNotesCheckScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_releaseNotesCheckScheduled = false;
if (!mounted || _activePageIdx != 0 || !_isViewActive()) return;
unawaited(
ReleaseNotesPresenter.showIfNeeded(context, releaseNotes060(context)),
);
});
}
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
_scheduleReleaseNotesIfNeeded();
if (AppState.hasCameraPermissions && if (AppState.hasCameraPermissions &&
_offsetRatio < 1 && _offsetRatio < 1 &&
!_mainCameraController.isSharePreviewIsShown && !_mainCameraController.isSharePreviewIsShown &&
@ -378,6 +409,9 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
_activePageIdx = pageIndex; _activePageIdx = pageIndex;
} }
}); });
if (notification is ScrollEndNotification) {
_scheduleReleaseNotesIfNeeded();
}
} }
if (AppState.hasCameraPermissions && if (AppState.hasCameraPermissions &&
@ -505,7 +539,10 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn, curve: Curves.bounceIn,
); );
if (mounted) setState(() {}); if (mounted) {
setState(() {});
_scheduleReleaseNotesIfNeeded();
}
}, },
currentIndex: _activePageIdx, currentIndex: _activePageIdx,
) )

View file

@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE contacts SET signal_version = 'v2', account_deleted = 0 WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "7cc39bf45c3395a1f71a40d027afb0b2b7a14f18fa0ed0c66e5c9cac5678f4d4"
}

View file

@ -332,6 +332,17 @@
"name": "twonly_score" "name": "twonly_score"
} }
} }
},
{
"name": "joined_at",
"ordinal": 30,
"type_info": "Integer",
"origin": {
"Table": {
"table": "contacts",
"name": "joined_at"
}
}
} }
], ],
"parameters": { "parameters": {
@ -367,6 +378,7 @@
false, false,
false, false,
false, false,
true,
true true
] ]
}, },

View file

@ -146,6 +146,13 @@ pub(crate) async fn handle_contact_request(
.build() .build()
.insert_on_conflict_update(tr) .insert_on_conflict_update(tr)
.await?; .await?;
if let Some(joined_at) = user.joined_at {
sqlx::query("UPDATE contacts SET joined_at = ? WHERE user_id = ?")
.bind(joined_at)
.bind(from_user_id)
.execute(&mut **tr)
.await?;
}
} }
} }
Type::Accept => { Type::Accept => {

View file

@ -134,6 +134,7 @@ message Response {
optional int64 signed_prekey_id = 6; optional int64 signed_prekey_id = 6;
optional int64 registration_id = 8; optional int64 registration_id = 8;
optional PqcBundle pqc_bundle = 9; optional PqcBundle pqc_bundle = 9;
optional int64 joined_at = 10;
} }
message UploadToken { message UploadToken {

View file

@ -38,6 +38,39 @@ impl Server {
}) })
} }
/// Loads the immutable account creation date for contacts that do not
/// already have it. Server-deleted contacts are deliberately excluded.
pub async fn refresh_contact_joined_dates(ctx: &Arc<Context>) -> Result<()> {
let database = ctx.app_db.read().await.clone();
let contact_ids = sqlx::query_scalar::<_, i64>(
"SELECT user_id FROM contacts WHERE account_deleted = 0 AND joined_at IS NULL",
)
.fetch_all(&database.pool)
.await?;
for contact_id in contact_ids {
let user = match Self::get_user_by_id(ctx, contact_id).await? {
ServerResult::Ok(user) => user,
// The shared API error handler marks UserIdNotFound contacts
// as deleted. Other per-contact errors should not prevent the
// remaining contacts from being refreshed.
ServerResult::ErrorCode(_) => continue,
};
let Some(joined_at) = user.joined_at else {
// Backwards compatibility while an older server is still
// running: leave the value NULL so a later startup retries it.
continue;
};
sqlx::query("UPDATE contacts SET joined_at = ? WHERE user_id = ?")
.bind(joined_at)
.bind(contact_id)
.execute(&database.pool)
.await?;
}
Ok(())
}
pub async fn check_for_deleted_usernames(ctx: &Arc<Context>) -> Result<()> { pub async fn check_for_deleted_usernames(ctx: &Arc<Context>) -> Result<()> {
let database = ctx.app_db.read().await.clone(); let database = ctx.app_db.read().await.clone();
let contacts = sqlx::query_scalar!( let contacts = sqlx::query_scalar!(

View file

@ -112,6 +112,7 @@ pub struct FrbUserData {
pub user_id: i64, pub user_id: i64,
pub username: Vec<u8>, pub username: Vec<u8>,
pub public_identity_key: Vec<u8>, pub public_identity_key: Vec<u8>,
pub joined_at: Option<i64>,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -593,8 +594,13 @@ impl RustApi {
user_id: res.user_id, user_id: res.user_id,
username: res.username.unwrap_or_default(), username: res.username.unwrap_or_default(),
public_identity_key: res.public_identity_key.unwrap_or_default(), public_identity_key: res.public_identity_key.unwrap_or_default(),
joined_at: res.joined_at,
}) })
} }
pub async fn refresh_contact_joined_dates() -> Result<()> {
let ctx = Context::get_static()?;
Server::refresh_contact_joined_dates(ctx).await
}
pub async fn check_for_deleted_usernames() -> Result<()> { pub async fn check_for_deleted_usernames() -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::check_for_deleted_usernames(ctx).await Server::check_for_deleted_usernames(ctx).await
@ -614,6 +620,7 @@ impl RustApi {
user_id: res.user_id, user_id: res.user_id,
username: res.username.unwrap_or_default(), username: res.username.unwrap_or_default(),
public_identity_key: res.public_identity_key.unwrap_or_default(), public_identity_key: res.public_identity_key.unwrap_or_default(),
joined_at: res.joined_at,
}) })
} }
pub async fn get_proof_of_work() -> Result<FrbProofOfWork> { pub async fn get_proof_of_work() -> Result<FrbProofOfWork> {

View file

@ -0,0 +1,3 @@
-- UTC Unix timestamp at which the contact registered with twonly. NULL until
-- it has been loaded from a server that supports this profile field.
ALTER TABLE contacts ADD COLUMN joined_at INTEGER;

View file

@ -16,7 +16,7 @@ mod legacy_import;
pub mod tables; pub mod tables;
pub const APP_DATABASE_FILE: &str = "app_db.sqlite"; pub const APP_DATABASE_FILE: &str = "app_db.sqlite";
pub const APP_SCHEMA_VERSION: i64 = 14; pub const APP_SCHEMA_VERSION: i64 = 15;
/// User-owned application tables in the current Rust schema. Rust-only outbox /// User-owned application tables in the current Rust schema. Rust-only outbox
/// tables are deliberately absent because they are reconstructed locally. /// tables are deliberately absent because they are reconstructed locally.
@ -402,6 +402,34 @@ mod change_notification_tests {
(directory, database) (directory, database)
} }
#[tokio::test]
async fn contact_joined_at_is_nullable_and_persisted() {
let (_dir, database) = open().await;
sqlx::query("INSERT INTO contacts(user_id, username) VALUES(7, 'alice')")
.execute(&database.pool)
.await
.unwrap();
let joined_at: Option<i64> =
sqlx::query_scalar("SELECT joined_at FROM contacts WHERE user_id = 7")
.fetch_one(&database.pool)
.await
.unwrap();
assert_eq!(joined_at, None);
sqlx::query("UPDATE contacts SET joined_at = ? WHERE user_id = 7")
.bind(1_700_000_000_i64)
.execute(&database.pool)
.await
.unwrap();
let joined_at: Option<i64> =
sqlx::query_scalar("SELECT joined_at FROM contacts WHERE user_id = 7")
.fetch_one(&database.pool)
.await
.unwrap();
assert_eq!(joined_at, Some(1_700_000_000));
}
fn insert(key: &str) -> String { fn insert(key: &str) -> String {
format!("INSERT INTO app_metadata(key, value) VALUES('{key}', '1')") format!("INSERT INTO app_metadata(key, value) VALUES('{key}', '1')")
} }

View file

@ -42,6 +42,7 @@ pub struct Contact {
pub widget_sharing_allowed: i64, pub widget_sharing_allowed: i64,
pub widget_sharing_granted: i64, pub widget_sharing_granted: i64,
pub twonly_score: Option<i64>, pub twonly_score: Option<i64>,
pub joined_at: Option<i64>,
} }
#[derive(bon::Builder)] #[derive(bon::Builder)]

View file

@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi, default_rust_auto_opaque = RustAutoOpaqueMoi,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1844664049; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1901082359;
// Section: executor // Section: executor
@ -3479,6 +3479,42 @@ fn wire__crate__bridge__api__rust_api_purge_widget_media_impl(
}, },
) )
} }
fn wire__crate__bridge__api__rust_api_refresh_contact_joined_dates_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::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "rust_api_refresh_contact_joined_dates",
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);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok =
crate::bridge::api::RustApi::refresh_contact_joined_dates().await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl( fn wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -7055,10 +7091,12 @@ impl SseDecode for crate::bridge::api::FrbUserData {
let mut var_userId = <i64>::sse_decode(deserializer); let mut var_userId = <i64>::sse_decode(deserializer);
let mut var_username = <Vec<u8>>::sse_decode(deserializer); let mut var_username = <Vec<u8>>::sse_decode(deserializer);
let mut var_publicIdentityKey = <Vec<u8>>::sse_decode(deserializer); let mut var_publicIdentityKey = <Vec<u8>>::sse_decode(deserializer);
let mut var_joinedAt = <Option<i64>>::sse_decode(deserializer);
return crate::bridge::api::FrbUserData { return crate::bridge::api::FrbUserData {
user_id: var_userId, user_id: var_userId,
username: var_username, username: var_username,
public_identity_key: var_publicIdentityKey, public_identity_key: var_publicIdentityKey,
joined_at: var_joinedAt,
}; };
} }
} }
@ -7944,6 +7982,7 @@ impl SseDecode for crate::user_config::UserConfig {
let mut var_currentSignedPreKeyIndexStart = <i64>::sse_decode(deserializer); let mut var_currentSignedPreKeyIndexStart = <i64>::sse_decode(deserializer);
let mut var_lastChangeLogHash = <Option<Vec<u8>>>::sse_decode(deserializer); let mut var_lastChangeLogHash = <Option<Vec<u8>>>::sse_decode(deserializer);
let mut var_hideChangeLog = <bool>::sse_decode(deserializer); let mut var_hideChangeLog = <bool>::sse_decode(deserializer);
let mut var_lastReleaseNotesVersion = <Option<String>>::sse_decode(deserializer);
let mut var_hideMemoriesBackupPromo = <bool>::sse_decode(deserializer); let mut var_hideMemoriesBackupPromo = <bool>::sse_decode(deserializer);
let mut var_hideWidgetShareExplainer = <bool>::sse_decode(deserializer); let mut var_hideWidgetShareExplainer = <bool>::sse_decode(deserializer);
let mut var_updateFcmToken = <bool>::sse_decode(deserializer); let mut var_updateFcmToken = <bool>::sse_decode(deserializer);
@ -8013,6 +8052,7 @@ impl SseDecode for crate::user_config::UserConfig {
current_signed_pre_key_index_start: var_currentSignedPreKeyIndexStart, current_signed_pre_key_index_start: var_currentSignedPreKeyIndexStart,
last_change_log_hash: var_lastChangeLogHash, last_change_log_hash: var_lastChangeLogHash,
hide_change_log: var_hideChangeLog, hide_change_log: var_hideChangeLog,
last_release_notes_version: var_lastReleaseNotesVersion,
hide_memories_backup_promo: var_hideMemoriesBackupPromo, hide_memories_backup_promo: var_hideMemoriesBackupPromo,
hide_widget_share_explainer: var_hideWidgetShareExplainer, hide_widget_share_explainer: var_hideWidgetShareExplainer,
update_fcm_token: var_updateFcmToken, update_fcm_token: var_updateFcmToken,
@ -8246,92 +8286,93 @@ fn pde_ffi_dispatcher_primary_impl(
91 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len), 91 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len),
92 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), 92 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
93 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), 93 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
94 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), 94 => wire__crate__bridge__api__rust_api_refresh_contact_joined_dates_impl(port, ptr, rust_vec_len, data_len),
95 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), 95 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
96 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), 96 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
97 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), 97 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
98 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), 98 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
99 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), 99 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
100 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), 100 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
101 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), 101 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
102 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), 102 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
103 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), 103 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
104 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), 104 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
105 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), 105 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
106 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), 106 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
107 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), 107 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
108 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), 108 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
109 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), 109 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
110 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), 110 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
111 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), 111 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
112 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), 112 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
113 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), 113 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
114 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), 114 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
115 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), 115 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
116 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), 116 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
117 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), 117 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
118 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), 118 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
119 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), 119 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
120 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), 120 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
121 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), 121 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
122 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), 122 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
123 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), 123 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
124 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), 124 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
125 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), 125 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
126 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), 126 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
127 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), 127 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
128 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), 128 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
129 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), 129 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
130 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), 130 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
131 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), 131 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
132 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), 132 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
133 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), 133 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
134 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), 134 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
135 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), 135 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
136 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), 136 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
137 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), 137 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
138 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), 138 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
139 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), 139 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
140 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), 140 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
141 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), 141 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
142 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), 142 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
143 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), 143 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
145 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), 145 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
146 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 146 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
147 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), 147 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), 149 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), 150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), 151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), 152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), 153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), 154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), 155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), 156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), 157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
158 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), 158 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
159 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), 159 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
160 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), 160 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
161 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), 161 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
162 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), 162 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
163 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len), 163 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
164 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), 164 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
165 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), 165 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
166 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len), 166 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
167 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), 167 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
168 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), 168 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
169 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), 169 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
170 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), 170 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
171 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len), 171 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
172 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), 172 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len),
173 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), 173 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
174 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), 174 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
176 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), 175 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
177 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), 177 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
178 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), 178 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
179 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), 179 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
180 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), 180 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
181 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -8350,12 +8391,12 @@ fn pde_ffi_dispatcher_sync_impl(
55 => { 55 => {
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
} }
175 => wire__crate__bridge__user_config__user_config_api_clone_impl( 176 => wire__crate__bridge__user_config__user_config_api_clone_impl(
ptr, ptr,
rust_vec_len, rust_vec_len,
data_len, data_len,
), ),
181 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), 182 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -8720,6 +8761,7 @@ impl flutter_rust_bridge::IntoDart for crate::bridge::api::FrbUserData {
self.user_id.into_into_dart().into_dart(), self.user_id.into_into_dart().into_dart(),
self.username.into_into_dart().into_dart(), self.username.into_into_dart().into_dart(),
self.public_identity_key.into_into_dart().into_dart(), self.public_identity_key.into_into_dart().into_dart(),
self.joined_at.into_into_dart().into_dart(),
] ]
.into_dart() .into_dart()
} }
@ -9321,6 +9363,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig {
.into_dart(), .into_dart(),
self.last_change_log_hash.into_into_dart().into_dart(), self.last_change_log_hash.into_into_dart().into_dart(),
self.hide_change_log.into_into_dart().into_dart(), self.hide_change_log.into_into_dart().into_dart(),
self.last_release_notes_version.into_into_dart().into_dart(),
self.hide_memories_backup_promo.into_into_dart().into_dart(), self.hide_memories_backup_promo.into_into_dart().into_dart(),
self.hide_widget_share_explainer self.hide_widget_share_explainer
.into_into_dart() .into_into_dart()
@ -9758,6 +9801,7 @@ impl SseEncode for crate::bridge::api::FrbUserData {
<i64>::sse_encode(self.user_id, serializer); <i64>::sse_encode(self.user_id, serializer);
<Vec<u8>>::sse_encode(self.username, serializer); <Vec<u8>>::sse_encode(self.username, serializer);
<Vec<u8>>::sse_encode(self.public_identity_key, serializer); <Vec<u8>>::sse_encode(self.public_identity_key, serializer);
<Option<i64>>::sse_encode(self.joined_at, serializer);
} }
} }
@ -10516,6 +10560,7 @@ impl SseEncode for crate::user_config::UserConfig {
<i64>::sse_encode(self.current_signed_pre_key_index_start, serializer); <i64>::sse_encode(self.current_signed_pre_key_index_start, serializer);
<Option<Vec<u8>>>::sse_encode(self.last_change_log_hash, serializer); <Option<Vec<u8>>>::sse_encode(self.last_change_log_hash, serializer);
<bool>::sse_encode(self.hide_change_log, serializer); <bool>::sse_encode(self.hide_change_log, serializer);
<Option<String>>::sse_encode(self.last_release_notes_version, serializer);
<bool>::sse_encode(self.hide_memories_backup_promo, serializer); <bool>::sse_encode(self.hide_memories_backup_promo, serializer);
<bool>::sse_encode(self.hide_widget_share_explainer, serializer); <bool>::sse_encode(self.hide_widget_share_explainer, serializer);
<bool>::sse_encode(self.update_fcm_token, serializer); <bool>::sse_encode(self.update_fcm_token, serializer);

View file

@ -51,6 +51,13 @@ impl ContactService {
.build() .build()
.insert_on_conflict_update(&mut transaction) .insert_on_conflict_update(&mut transaction)
.await?; .await?;
if let Some(joined_at) = user.joined_at {
sqlx::query("UPDATE contacts SET joined_at = ? WHERE user_id = ?")
.bind(joined_at)
.bind(user.user_id)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?; transaction.commit().await?;
self.send_contact_request( self.send_contact_request(
@ -113,10 +120,12 @@ impl ContactService {
let database = self.ctx.app_db.read().await.clone(); let database = self.ctx.app_db.read().await.clone();
// The server answered with a bundle, so a previous `UserIdNotFound` // The server answered with a bundle, so a previous `UserIdNotFound`
// (or a manual mark) must not keep the contact blocked. // (or a manual mark) must not keep the contact blocked.
sqlx::query!( sqlx::query(
"UPDATE contacts SET signal_version = 'v2', account_deleted = 0 WHERE user_id = ?", "UPDATE contacts SET signal_version = 'v2', account_deleted = 0, \
user_id joined_at = COALESCE(?, joined_at) WHERE user_id = ?",
) )
.bind(user.joined_at)
.bind(user_id)
.execute(&database.pool) .execute(&database.pool)
.await?; .await?;
release_deferred_receipts(&database, user_id).await?; release_deferred_receipts(&database, user_id).await?;

View file

@ -361,6 +361,10 @@ pub struct UserConfig {
#[serde(default = "defaults::true_value")] #[serde(default = "defaults::true_value")]
#[frb(non_final)] #[frb(non_final)]
pub hide_change_log: bool, pub hide_change_log: bool,
/// Last in-app release note the user dismissed.
#[serde(default)]
#[frb(non_final)]
pub last_release_notes_version: Option<String>,
#[serde(default)] #[serde(default)]
#[frb(non_final)] #[frb(non_final)]
pub hide_memories_backup_promo: bool, pub hide_memories_backup_promo: bool,
@ -551,6 +555,7 @@ mod tests {
); );
assert!(!config.is_user_discovery_enabled); assert!(!config.is_user_discovery_enabled);
assert_eq!(config.last_server_message_at, None); assert_eq!(config.last_server_message_at, None);
assert_eq!(config.last_release_notes_version, None);
} }
#[test] #[test]

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/src/visual/components/release_notes.comp.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart';
const _releaseNotes = ReleaseNotesData(
version: '1.2.3',
title: "What's new",
introduction: 'A short introduction.',
additionalChangesTitle: 'Also improved',
closeLabel: 'Close',
features: [
ReleaseNoteFeature(
icon: Icon(Icons.star_rounded),
title: 'First feature',
description: 'A short feature description.',
),
],
additionalChanges: ['One more improvement'],
);
void main() {
testWidgets('shows generic release notes at 80 percent height', (
tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) => TextButton(
onPressed: () => showReleaseNotesBottomSheet(
context,
_releaseNotes,
),
child: const Text('Open'),
),
),
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('v1.2.3'), findsOneWidget);
expect(find.text("What's new"), findsOneWidget);
expect(find.text('First feature'), findsOneWidget);
expect(find.text('One more improvement'), findsOneWidget);
expect(find.byType(MyButton), findsOneWidget);
final sheet = find.byType(ReleaseNotesBottomSheet);
final mediaSize = MediaQuery.sizeOf(tester.element(sheet));
expect(tester.getSize(sheet).height, closeTo(mediaSize.height * 0.8, 0.1));
await tester.tap(find.byType(MyButton));
await tester.pumpAndSettle();
expect(sheet, findsNothing);
});
}

View file

@ -0,0 +1,48 @@
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/utils/misc.dart';
void main() {
testWidgets('formats contact join age into friendly calendar ranges', (
tester,
) async {
final now = DateTime.utc(2026, 10, 5, 12);
late BuildContext context;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (buildContext) {
context = buildContext;
return const SizedBox.shrink();
},
),
),
);
withClock(Clock.fixed(now), () {
expect(formatJoinedAt(context, now), 'Joined today');
expect(
formatJoinedAt(context, now.subtract(const Duration(days: 20))),
'Joined 20 days ago',
);
expect(
formatJoinedAt(context, now.subtract(const Duration(days: 60))),
'Joined 2 months ago',
);
expect(
formatJoinedAt(context, now.subtract(const Duration(days: 730))),
'Joined 2 years ago',
);
});
});
}