mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
add joined chip to contact view
This commit is contained in:
parent
e76eead183
commit
ced68167d1
38 changed files with 1539 additions and 243 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -1,5 +1,17 @@
|
|||
# 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
|
||||
|
||||
- Fix: Recover broken sessions more reliably
|
||||
|
|
|
|||
|
|
@ -245,16 +245,21 @@ class FrbUserData {
|
|||
final PlatformInt64 userId;
|
||||
final Uint8List username;
|
||||
final Uint8List publicIdentityKey;
|
||||
final PlatformInt64? joinedAt;
|
||||
|
||||
const FrbUserData({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.publicIdentityKey,
|
||||
this.joinedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
userId.hashCode ^ username.hashCode ^ publicIdentityKey.hashCode;
|
||||
userId.hashCode ^
|
||||
username.hashCode ^
|
||||
publicIdentityKey.hashCode ^
|
||||
joinedAt.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
|
|
@ -263,7 +268,8 @@ class FrbUserData {
|
|||
runtimeType == other.runtimeType &&
|
||||
userId == other.userId &&
|
||||
username == other.username &&
|
||||
publicIdentityKey == other.publicIdentityKey;
|
||||
publicIdentityKey == other.publicIdentityKey &&
|
||||
joinedAt == other.joinedAt;
|
||||
}
|
||||
|
||||
/// Flutter-facing facade for the Rust-owned API runtime.
|
||||
|
|
@ -597,6 +603,9 @@ class RustApi {
|
|||
static Future<void> purgeWidgetMedia() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiPurgeWidgetMedia();
|
||||
|
||||
static Future<void> refreshContactJoinedDates() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiRefreshContactJoinedDates();
|
||||
|
||||
/// Republishes the widget manifest.
|
||||
///
|
||||
/// The contact groups a widget offers in its configuration UI are read from
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -1844664049;
|
||||
int get rustContentHash => -1901082359;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
|
|
@ -417,6 +417,8 @@ abstract class RustLibApi extends BaseApi {
|
|||
|
||||
Future<void> crateBridgeApiRustApiPurgeWidgetMedia();
|
||||
|
||||
Future<void> crateBridgeApiRustApiRefreshContactJoinedDates();
|
||||
|
||||
Future<void> crateBridgeApiRustApiRefreshWidgetManifest();
|
||||
|
||||
Future<PlatformInt64> crateBridgeApiRustApiRegister({
|
||||
|
|
@ -3891,7 +3893,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeApiRustApiRefreshWidgetManifest() {
|
||||
Future<void> crateBridgeApiRustApiRefreshContactJoinedDates() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
|
|
@ -3907,6 +3909,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
decodeSuccessData: sse_decode_unit,
|
||||
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,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
|
|
@ -3938,7 +3970,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 95,
|
||||
funcId: 96,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3973,7 +4005,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 96,
|
||||
funcId: 97,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4012,7 +4044,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 97,
|
||||
funcId: 98,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4054,7 +4086,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 98,
|
||||
funcId: 99,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4085,7 +4117,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 99,
|
||||
funcId: 100,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4118,7 +4150,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 100,
|
||||
funcId: 101,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4151,7 +4183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 101,
|
||||
funcId: 102,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4186,7 +4218,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 102,
|
||||
funcId: 103,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4219,7 +4251,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 103,
|
||||
funcId: 104,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4252,7 +4284,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 104,
|
||||
funcId: 105,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4285,7 +4317,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 105,
|
||||
funcId: 106,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4322,7 +4354,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 106,
|
||||
funcId: 107,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4352,7 +4384,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 107,
|
||||
funcId: 108,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4382,7 +4414,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 108,
|
||||
funcId: 109,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4412,7 +4444,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 109,
|
||||
funcId: 110,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4445,7 +4477,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 110,
|
||||
funcId: 111,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4476,7 +4508,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 111,
|
||||
funcId: 112,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4509,7 +4541,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 112,
|
||||
funcId: 113,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4555,7 +4587,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 113,
|
||||
funcId: 114,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4611,7 +4643,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 114,
|
||||
funcId: 115,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4661,7 +4693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 115,
|
||||
funcId: 116,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4706,7 +4738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 116,
|
||||
funcId: 117,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4741,7 +4773,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 117,
|
||||
funcId: 118,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4776,7 +4808,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 118,
|
||||
funcId: 119,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4809,7 +4841,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 119,
|
||||
funcId: 120,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4846,7 +4878,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 120,
|
||||
funcId: 121,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4877,7 +4909,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 121,
|
||||
funcId: 122,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4915,7 +4947,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 122,
|
||||
funcId: 123,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4950,7 +4982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 123,
|
||||
funcId: 124,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4989,7 +5021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 124,
|
||||
funcId: 125,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5022,7 +5054,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 125,
|
||||
funcId: 126,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5053,7 +5085,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 126,
|
||||
funcId: 127,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5088,7 +5120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 127,
|
||||
funcId: 128,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5118,7 +5150,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 128,
|
||||
funcId: 129,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5151,7 +5183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 129,
|
||||
funcId: 130,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5186,7 +5218,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 130,
|
||||
funcId: 131,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5219,7 +5251,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 131,
|
||||
funcId: 132,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5250,7 +5282,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 132,
|
||||
funcId: 133,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5287,7 +5319,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 133,
|
||||
funcId: 134,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5339,7 +5371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 134,
|
||||
funcId: 135,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5392,7 +5424,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 135,
|
||||
funcId: 136,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5432,7 +5464,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 136,
|
||||
funcId: 137,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5465,7 +5497,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 137,
|
||||
funcId: 138,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5498,7 +5530,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 138,
|
||||
funcId: 139,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5535,7 +5567,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 139,
|
||||
funcId: 140,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5567,7 +5599,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 140,
|
||||
funcId: 141,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5600,7 +5632,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 141,
|
||||
funcId: 142,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5635,7 +5667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 142,
|
||||
funcId: 143,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5667,7 +5699,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 143,
|
||||
funcId: 144,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5705,7 +5737,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 144,
|
||||
funcId: 145,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5738,7 +5770,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 145,
|
||||
funcId: 146,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5776,7 +5808,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 146,
|
||||
funcId: 147,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5813,7 +5845,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 147,
|
||||
funcId: 148,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5850,7 +5882,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 148,
|
||||
funcId: 149,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5888,7 +5920,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 149,
|
||||
funcId: 150,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5926,7 +5958,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 150,
|
||||
funcId: 151,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5959,7 +5991,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 151,
|
||||
funcId: 152,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5991,7 +6023,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 152,
|
||||
funcId: 153,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6026,7 +6058,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 153,
|
||||
funcId: 154,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6063,7 +6095,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 154,
|
||||
funcId: 155,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6096,7 +6128,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 155,
|
||||
funcId: 156,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6128,7 +6160,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 156,
|
||||
funcId: 157,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6163,7 +6195,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 157,
|
||||
funcId: 158,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6202,7 +6234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 158,
|
||||
funcId: 159,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6239,7 +6271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 159,
|
||||
funcId: 160,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6269,7 +6301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 160,
|
||||
funcId: 161,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6301,7 +6333,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 161,
|
||||
funcId: 162,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6336,7 +6368,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 162,
|
||||
funcId: 163,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6371,7 +6403,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 163,
|
||||
funcId: 164,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6402,7 +6434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 164,
|
||||
funcId: 165,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6440,7 +6472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 165,
|
||||
funcId: 166,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6475,7 +6507,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 166,
|
||||
funcId: 167,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6514,7 +6546,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 167,
|
||||
funcId: 168,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6549,7 +6581,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 168,
|
||||
funcId: 169,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6594,7 +6626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 169,
|
||||
funcId: 170,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6637,7 +6669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 170,
|
||||
funcId: 171,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6669,7 +6701,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 171,
|
||||
funcId: 172,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6704,7 +6736,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 172,
|
||||
funcId: 173,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6739,7 +6771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 173,
|
||||
funcId: 174,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6774,7 +6806,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 174,
|
||||
funcId: 175,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6807,7 +6839,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 175,
|
||||
funcId: 176,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
@ -6847,7 +6879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 176,
|
||||
funcId: 177,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6892,7 +6924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 177,
|
||||
funcId: 178,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6922,7 +6954,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 178,
|
||||
funcId: 179,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6955,7 +6987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 179,
|
||||
funcId: 180,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6990,7 +7022,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 180,
|
||||
funcId: 181,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -7029,7 +7061,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 181,
|
||||
funcId: 182,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
@ -7498,12 +7530,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
FrbUserData dco_decode_frb_user_data(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 3)
|
||||
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return FrbUserData(
|
||||
userId: dco_decode_i_64(arr[0]),
|
||||
username: dco_decode_list_prim_u_8_strict(arr[1]),
|
||||
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) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 65)
|
||||
throw Exception('unexpected arr length: expect 65 but see ${arr.length}');
|
||||
if (arr.length != 66)
|
||||
throw Exception('unexpected arr length: expect 66 but see ${arr.length}');
|
||||
return UserConfig(
|
||||
userId: dco_decode_i_64(arr[0]),
|
||||
username: dco_decode_String(arr[1]),
|
||||
|
|
@ -8187,20 +8220,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
currentSignedPreKeyIndexStart: dco_decode_i_64(arr[49]),
|
||||
lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[50]),
|
||||
hideChangeLog: dco_decode_bool(arr[51]),
|
||||
hideMemoriesBackupPromo: dco_decode_bool(arr[52]),
|
||||
hideWidgetShareExplainer: dco_decode_bool(arr[53]),
|
||||
updateFcmToken: dco_decode_bool(arr[54]),
|
||||
canUseLoginTokenForAuth: dco_decode_bool(arr[55]),
|
||||
twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[56]),
|
||||
isBackupEnabled: dco_decode_bool(arr[57]),
|
||||
lastReleaseNotesVersion: dco_decode_opt_String(arr[52]),
|
||||
hideMemoriesBackupPromo: dco_decode_bool(arr[53]),
|
||||
hideWidgetShareExplainer: dco_decode_bool(arr[54]),
|
||||
updateFcmToken: dco_decode_bool(arr[55]),
|
||||
canUseLoginTokenForAuth: dco_decode_bool(arr[56]),
|
||||
twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[57]),
|
||||
isBackupEnabled: dco_decode_bool(arr[58]),
|
||||
passwordLessRecovery:
|
||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[58]),
|
||||
fcmToken: dco_decode_opt_String(arr[59]),
|
||||
lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[60]),
|
||||
lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[61]),
|
||||
currentSetupPage: dco_decode_opt_String(arr[62]),
|
||||
skipSetupPages: dco_decode_bool(arr[63]),
|
||||
hasZoomed: dco_decode_bool(arr[64]),
|
||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[59]),
|
||||
fcmToken: dco_decode_opt_String(arr[60]),
|
||||
lastFcmWakeupAt: 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[63]),
|
||||
skipSetupPages: 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_username = 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(
|
||||
userId: var_userId,
|
||||
username: var_username,
|
||||
publicIdentityKey: var_publicIdentityKey,
|
||||
joinedAt: var_joinedAt,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -9566,6 +9602,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
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_hideWidgetShareExplainer = sse_decode_bool(deserializer);
|
||||
var var_updateFcmToken = sse_decode_bool(deserializer);
|
||||
|
|
@ -9638,6 +9675,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
currentSignedPreKeyIndexStart: var_currentSignedPreKeyIndexStart,
|
||||
lastChangeLogHash: var_lastChangeLogHash,
|
||||
hideChangeLog: var_hideChangeLog,
|
||||
lastReleaseNotesVersion: var_lastReleaseNotesVersion,
|
||||
hideMemoriesBackupPromo: var_hideMemoriesBackupPromo,
|
||||
hideWidgetShareExplainer: var_hideWidgetShareExplainer,
|
||||
updateFcmToken: var_updateFcmToken,
|
||||
|
|
@ -10209,6 +10247,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
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.publicIdentityKey, serializer);
|
||||
sse_encode_opt_box_autoadd_i_64(self.joinedAt, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
|
|
@ -10984,6 +11023,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
sse_encode_i_64(self.currentSignedPreKeyIndexStart, serializer);
|
||||
sse_encode_opt_list_prim_u_8_strict(self.lastChangeLogHash, serializer);
|
||||
sse_encode_bool(self.hideChangeLog, serializer);
|
||||
sse_encode_opt_String(self.lastReleaseNotesVersion, serializer);
|
||||
sse_encode_bool(self.hideMemoriesBackupPromo, serializer);
|
||||
sse_encode_bool(self.hideWidgetShareExplainer, serializer);
|
||||
sse_encode_bool(self.updateFcmToken, serializer);
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ class UserConfig {
|
|||
PlatformInt64 currentSignedPreKeyIndexStart;
|
||||
Uint8List? lastChangeLogHash;
|
||||
bool hideChangeLog;
|
||||
|
||||
/// Last in-app release note the user dismissed.
|
||||
String? lastReleaseNotesVersion;
|
||||
bool hideMemoriesBackupPromo;
|
||||
bool hideWidgetShareExplainer;
|
||||
bool updateFcmToken;
|
||||
|
|
@ -250,6 +253,7 @@ class UserConfig {
|
|||
required this.currentSignedPreKeyIndexStart,
|
||||
this.lastChangeLogHash,
|
||||
required this.hideChangeLog,
|
||||
this.lastReleaseNotesVersion,
|
||||
required this.hideMemoriesBackupPromo,
|
||||
required this.hideWidgetShareExplainer,
|
||||
required this.updateFcmToken,
|
||||
|
|
@ -319,6 +323,7 @@ class UserConfig {
|
|||
currentSignedPreKeyIndexStart.hashCode ^
|
||||
lastChangeLogHash.hashCode ^
|
||||
hideChangeLog.hashCode ^
|
||||
lastReleaseNotesVersion.hashCode ^
|
||||
hideMemoriesBackupPromo.hashCode ^
|
||||
hideWidgetShareExplainer.hashCode ^
|
||||
updateFcmToken.hashCode ^
|
||||
|
|
@ -397,6 +402,7 @@ class UserConfig {
|
|||
other.currentSignedPreKeyIndexStart &&
|
||||
lastChangeLogHash == other.lastChangeLogHash &&
|
||||
hideChangeLog == other.hideChangeLog &&
|
||||
lastReleaseNotesVersion == other.lastReleaseNotesVersion &&
|
||||
hideMemoriesBackupPromo == other.hideMemoriesBackupPromo &&
|
||||
hideWidgetShareExplainer == other.hideWidgetShareExplainer &&
|
||||
updateFcmToken == other.updateFcmToken &&
|
||||
|
|
|
|||
|
|
@ -310,6 +310,10 @@ Future<void> postStartupTasks() async {
|
|||
RustApi.finishStartedMediaUploads(),
|
||||
'finishStartedMediaUploads',
|
||||
);
|
||||
unawaitedRustCall(
|
||||
RustApi.refreshContactJoinedDates(),
|
||||
'refreshContactJoinedDates',
|
||||
);
|
||||
unawaited(
|
||||
newsService.init().then((_) {
|
||||
final lastDownload = newsService.lastDownloadedAt;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class Contacts extends Table {
|
|||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get twonlyScore => integer().nullable()();
|
||||
DateTimeColumn get joinedAt => dateTime().nullable()();
|
||||
|
||||
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
|
||||
IntColumn get mediaReceivedCounter =>
|
||||
|
|
|
|||
|
|
@ -348,6 +348,17 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
type: DriftSqlType.int,
|
||||
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(
|
||||
'mediaSendCounter',
|
||||
);
|
||||
|
|
@ -400,6 +411,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
widgetSharingAllowed,
|
||||
widgetSharingGranted,
|
||||
twonlyScore,
|
||||
joinedAt,
|
||||
mediaSendCounter,
|
||||
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')) {
|
||||
context.handle(
|
||||
_mediaSendCounterMeta,
|
||||
|
|
@ -764,6 +782,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
|
|||
DriftSqlType.int,
|
||||
data['${effectivePrefix}twonly_score'],
|
||||
),
|
||||
joinedAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}joined_at'],
|
||||
),
|
||||
mediaSendCounter: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.int,
|
||||
data['${effectivePrefix}media_send_counter'],
|
||||
|
|
@ -814,6 +836,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
final bool widgetSharingAllowed;
|
||||
final bool widgetSharingGranted;
|
||||
final int? twonlyScore;
|
||||
final DateTime? joinedAt;
|
||||
final int mediaSendCounter;
|
||||
final int mediaReceivedCounter;
|
||||
const Contact({
|
||||
|
|
@ -844,6 +867,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
required this.widgetSharingAllowed,
|
||||
required this.widgetSharingGranted,
|
||||
this.twonlyScore,
|
||||
this.joinedAt,
|
||||
required this.mediaSendCounter,
|
||||
required this.mediaReceivedCounter,
|
||||
});
|
||||
|
|
@ -915,6 +939,9 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
if (!nullToAbsent || twonlyScore != null) {
|
||||
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_received_counter'] = Variable<int>(mediaReceivedCounter);
|
||||
return map;
|
||||
|
|
@ -977,6 +1004,9 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
twonlyScore: twonlyScore == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(twonlyScore),
|
||||
joinedAt: joinedAt == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(joinedAt),
|
||||
mediaSendCounter: Value(mediaSendCounter),
|
||||
mediaReceivedCounter: Value(mediaReceivedCounter),
|
||||
);
|
||||
|
|
@ -1045,6 +1075,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
json['widgetSharingGranted'],
|
||||
),
|
||||
twonlyScore: serializer.fromJson<int?>(json['twonlyScore']),
|
||||
joinedAt: serializer.fromJson<DateTime?>(json['joinedAt']),
|
||||
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
|
||||
mediaReceivedCounter: serializer.fromJson<int>(
|
||||
json['mediaReceivedCounter'],
|
||||
|
|
@ -1100,6 +1131,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
'widgetSharingAllowed': serializer.toJson<bool>(widgetSharingAllowed),
|
||||
'widgetSharingGranted': serializer.toJson<bool>(widgetSharingGranted),
|
||||
'twonlyScore': serializer.toJson<int?>(twonlyScore),
|
||||
'joinedAt': serializer.toJson<DateTime?>(joinedAt),
|
||||
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
|
||||
'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter),
|
||||
};
|
||||
|
|
@ -1133,6 +1165,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
bool? widgetSharingAllowed,
|
||||
bool? widgetSharingGranted,
|
||||
Value<int?> twonlyScore = const Value.absent(),
|
||||
Value<DateTime?> joinedAt = const Value.absent(),
|
||||
int? mediaSendCounter,
|
||||
int? mediaReceivedCounter,
|
||||
}) => Contact(
|
||||
|
|
@ -1182,6 +1215,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed,
|
||||
widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted,
|
||||
twonlyScore: twonlyScore.present ? twonlyScore.value : this.twonlyScore,
|
||||
joinedAt: joinedAt.present ? joinedAt.value : this.joinedAt,
|
||||
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
|
||||
mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter,
|
||||
);
|
||||
|
|
@ -1252,6 +1286,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
twonlyScore: data.twonlyScore.present
|
||||
? data.twonlyScore.value
|
||||
: this.twonlyScore,
|
||||
joinedAt: data.joinedAt.present ? data.joinedAt.value : this.joinedAt,
|
||||
mediaSendCounter: data.mediaSendCounter.present
|
||||
? data.mediaSendCounter.value
|
||||
: this.mediaSendCounter,
|
||||
|
|
@ -1293,6 +1328,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
..write('widgetSharingAllowed: $widgetSharingAllowed, ')
|
||||
..write('widgetSharingGranted: $widgetSharingGranted, ')
|
||||
..write('twonlyScore: $twonlyScore, ')
|
||||
..write('joinedAt: $joinedAt, ')
|
||||
..write('mediaSendCounter: $mediaSendCounter, ')
|
||||
..write('mediaReceivedCounter: $mediaReceivedCounter')
|
||||
..write(')'))
|
||||
|
|
@ -1328,6 +1364,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
widgetSharingAllowed,
|
||||
widgetSharingGranted,
|
||||
twonlyScore,
|
||||
joinedAt,
|
||||
mediaSendCounter,
|
||||
mediaReceivedCounter,
|
||||
]);
|
||||
|
|
@ -1376,6 +1413,7 @@ class Contact extends DataClass implements Insertable<Contact> {
|
|||
other.widgetSharingAllowed == this.widgetSharingAllowed &&
|
||||
other.widgetSharingGranted == this.widgetSharingGranted &&
|
||||
other.twonlyScore == this.twonlyScore &&
|
||||
other.joinedAt == this.joinedAt &&
|
||||
other.mediaSendCounter == this.mediaSendCounter &&
|
||||
other.mediaReceivedCounter == this.mediaReceivedCounter);
|
||||
}
|
||||
|
|
@ -1408,6 +1446,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
final Value<bool> widgetSharingAllowed;
|
||||
final Value<bool> widgetSharingGranted;
|
||||
final Value<int?> twonlyScore;
|
||||
final Value<DateTime?> joinedAt;
|
||||
final Value<int> mediaSendCounter;
|
||||
final Value<int> mediaReceivedCounter;
|
||||
const ContactsCompanion({
|
||||
|
|
@ -1438,6 +1477,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
this.widgetSharingAllowed = const Value.absent(),
|
||||
this.widgetSharingGranted = const Value.absent(),
|
||||
this.twonlyScore = const Value.absent(),
|
||||
this.joinedAt = const Value.absent(),
|
||||
this.mediaSendCounter = const Value.absent(),
|
||||
this.mediaReceivedCounter = const Value.absent(),
|
||||
});
|
||||
|
|
@ -1469,6 +1509,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
this.widgetSharingAllowed = const Value.absent(),
|
||||
this.widgetSharingGranted = const Value.absent(),
|
||||
this.twonlyScore = const Value.absent(),
|
||||
this.joinedAt = const Value.absent(),
|
||||
this.mediaSendCounter = const Value.absent(),
|
||||
this.mediaReceivedCounter = const Value.absent(),
|
||||
}) : username = Value(username);
|
||||
|
|
@ -1500,6 +1541,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
Expression<bool>? widgetSharingAllowed,
|
||||
Expression<bool>? widgetSharingGranted,
|
||||
Expression<int>? twonlyScore,
|
||||
Expression<DateTime>? joinedAt,
|
||||
Expression<int>? mediaSendCounter,
|
||||
Expression<int>? mediaReceivedCounter,
|
||||
}) {
|
||||
|
|
@ -1545,6 +1587,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
if (widgetSharingGranted != null)
|
||||
'widget_sharing_granted': widgetSharingGranted,
|
||||
if (twonlyScore != null) 'twonly_score': twonlyScore,
|
||||
if (joinedAt != null) 'joined_at': joinedAt,
|
||||
if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter,
|
||||
if (mediaReceivedCounter != null)
|
||||
'media_received_counter': mediaReceivedCounter,
|
||||
|
|
@ -1579,6 +1622,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
Value<bool>? widgetSharingAllowed,
|
||||
Value<bool>? widgetSharingGranted,
|
||||
Value<int?>? twonlyScore,
|
||||
Value<DateTime?>? joinedAt,
|
||||
Value<int>? mediaSendCounter,
|
||||
Value<int>? mediaReceivedCounter,
|
||||
}) {
|
||||
|
|
@ -1618,6 +1662,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed,
|
||||
widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted,
|
||||
twonlyScore: twonlyScore ?? this.twonlyScore,
|
||||
joinedAt: joinedAt ?? this.joinedAt,
|
||||
mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter,
|
||||
mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter,
|
||||
);
|
||||
|
|
@ -1735,6 +1780,9 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
if (twonlyScore.present) {
|
||||
map['twonly_score'] = Variable<int>(twonlyScore.value);
|
||||
}
|
||||
if (joinedAt.present) {
|
||||
map['joined_at'] = Variable<DateTime>(joinedAt.value);
|
||||
}
|
||||
if (mediaSendCounter.present) {
|
||||
map['media_send_counter'] = Variable<int>(mediaSendCounter.value);
|
||||
}
|
||||
|
|
@ -1776,6 +1824,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
|
|||
..write('widgetSharingAllowed: $widgetSharingAllowed, ')
|
||||
..write('widgetSharingGranted: $widgetSharingGranted, ')
|
||||
..write('twonlyScore: $twonlyScore, ')
|
||||
..write('joinedAt: $joinedAt, ')
|
||||
..write('mediaSendCounter: $mediaSendCounter, ')
|
||||
..write('mediaReceivedCounter: $mediaReceivedCounter')
|
||||
..write(')'))
|
||||
|
|
@ -15812,6 +15861,7 @@ typedef $$ContactsTableCreateCompanionBuilder =
|
|||
Value<bool> widgetSharingAllowed,
|
||||
Value<bool> widgetSharingGranted,
|
||||
Value<int?> twonlyScore,
|
||||
Value<DateTime?> joinedAt,
|
||||
Value<int> mediaSendCounter,
|
||||
Value<int> mediaReceivedCounter,
|
||||
});
|
||||
|
|
@ -15844,6 +15894,7 @@ typedef $$ContactsTableUpdateCompanionBuilder =
|
|||
Value<bool> widgetSharingAllowed,
|
||||
Value<bool> widgetSharingGranted,
|
||||
Value<int?> twonlyScore,
|
||||
Value<DateTime?> joinedAt,
|
||||
Value<int> mediaSendCounter,
|
||||
Value<int> mediaReceivedCounter,
|
||||
});
|
||||
|
|
@ -16268,6 +16319,11 @@ class $$ContactsTableFilterComposer
|
|||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get joinedAt => $composableBuilder(
|
||||
column: $table.joinedAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<int> get mediaSendCounter => $composableBuilder(
|
||||
column: $table.mediaSendCounter,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
|
|
@ -16733,6 +16789,11 @@ class $$ContactsTableOrderingComposer
|
|||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get joinedAt => $composableBuilder(
|
||||
column: $table.joinedAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<int> get mediaSendCounter => $composableBuilder(
|
||||
column: $table.mediaSendCounter,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
|
|
@ -16875,6 +16936,9 @@ class $$ContactsTableAnnotationComposer
|
|||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get joinedAt =>
|
||||
$composableBuilder(column: $table.joinedAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get mediaSendCounter => $composableBuilder(
|
||||
column: $table.mediaSendCounter,
|
||||
builder: (column) => column,
|
||||
|
|
@ -17270,6 +17334,7 @@ class $$ContactsTableTableManager
|
|||
Value<bool> widgetSharingAllowed = const Value.absent(),
|
||||
Value<bool> widgetSharingGranted = const Value.absent(),
|
||||
Value<int?> twonlyScore = const Value.absent(),
|
||||
Value<DateTime?> joinedAt = const Value.absent(),
|
||||
Value<int> mediaSendCounter = const Value.absent(),
|
||||
Value<int> mediaReceivedCounter = const Value.absent(),
|
||||
}) => ContactsCompanion(
|
||||
|
|
@ -17300,6 +17365,7 @@ class $$ContactsTableTableManager
|
|||
widgetSharingAllowed: widgetSharingAllowed,
|
||||
widgetSharingGranted: widgetSharingGranted,
|
||||
twonlyScore: twonlyScore,
|
||||
joinedAt: joinedAt,
|
||||
mediaSendCounter: mediaSendCounter,
|
||||
mediaReceivedCounter: mediaReceivedCounter,
|
||||
),
|
||||
|
|
@ -17334,6 +17400,7 @@ class $$ContactsTableTableManager
|
|||
Value<bool> widgetSharingAllowed = const Value.absent(),
|
||||
Value<bool> widgetSharingGranted = const Value.absent(),
|
||||
Value<int?> twonlyScore = const Value.absent(),
|
||||
Value<DateTime?> joinedAt = const Value.absent(),
|
||||
Value<int> mediaSendCounter = const Value.absent(),
|
||||
Value<int> mediaReceivedCounter = const Value.absent(),
|
||||
}) => ContactsCompanion.insert(
|
||||
|
|
@ -17364,6 +17431,7 @@ class $$ContactsTableTableManager
|
|||
widgetSharingAllowed: widgetSharingAllowed,
|
||||
widgetSharingGranted: widgetSharingGranted,
|
||||
twonlyScore: twonlyScore,
|
||||
joinedAt: joinedAt,
|
||||
mediaSendCounter: mediaSendCounter,
|
||||
mediaReceivedCounter: mediaReceivedCounter,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -5169,6 +5193,96 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Delete sticker'**
|
||||
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
|
||||
|
|
|
|||
|
|
@ -316,6 +316,51 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||
String get contactTwonlyScoreDescription =>
|
||||
'تعكس نقاط 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
|
||||
String get privacyVisibilityNobody => 'لا أحد';
|
||||
|
||||
|
|
@ -3043,4 +3088,54 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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 => 'اللغة العربية (تجريبية)';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,6 +319,42 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
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.';
|
||||
|
||||
@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
|
||||
String get privacyVisibilityNobody => 'Niemand';
|
||||
|
||||
|
|
@ -3045,4 +3081,55 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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)';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,42 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
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.';
|
||||
|
||||
@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
|
||||
String get privacyVisibilityNobody => 'Nobody';
|
||||
|
||||
|
|
@ -3014,4 +3050,55 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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)';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@
|
|||
"publicProfilePrivacyDescription": "اختر من يمكنه رؤية معلومات ملفك الشخصي.",
|
||||
"publicProfileTwonlyScore": "نقاط 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": "لا أحد",
|
||||
"privacyVisibilityOnlyContacts": "جهات الاتصال فقط",
|
||||
"privacyVisibilityEveryone": "الجميع",
|
||||
|
|
@ -865,5 +869,20 @@
|
|||
"notificationSticker": "أرسل ملصقًا{inGroup}.",
|
||||
"storeSticker": "حفظ الملصق",
|
||||
"removeSticker": "إزالة من المجموعة",
|
||||
"deleteSticker": "حذف الملصق"
|
||||
"deleteSticker": "حذف الملصق",
|
||||
"releaseNotesTitle": "ما الجديد؟",
|
||||
"releaseNotesIntroduction": "وصل أكبر تحديث لـ twonly حتى الآن. إليك نظرة سريعة على الجديد.",
|
||||
"releaseNotesAlsoImproved": "تحسينات أخرى",
|
||||
"releaseNotesStickers": "الملصقات",
|
||||
"releaseNotesStickersDescription": "حوّل صورك إلى ملصقات وشاركها في المحادثات.",
|
||||
"releaseNotesWidgets": "الأدوات",
|
||||
"releaseNotesWidgetsDescription": "شاهد أحدث صور الأشخاص المفضلين لديك مباشرة على شاشتك الرئيسية.",
|
||||
"releaseNotesStories": "القصص",
|
||||
"releaseNotesStoriesDescription": "شارك لحظاتك مع جهات اتصالك لمدة 24 ساعة.",
|
||||
"releaseNotesMiniGames": "ألعاب مصغّرة في المحادثات والمجموعات",
|
||||
"releaseNotesMiniGamesDescription": "العبوا ألعابًا صغيرة معًا داخل محادثاتكم مباشرة.",
|
||||
"releaseNotesReliableMessages": "موثوقية أفضل للرسائل والإشعارات",
|
||||
"releaseNotesImprovedUi": "واجهة مستخدم محسّنة",
|
||||
"releaseNotesCustomAvatars": "صور رمزية مخصّصة",
|
||||
"releaseNotesArabic": "اللغة العربية (تجريبية)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,10 @@
|
|||
"publicProfilePrivacyDescription": "Wähle aus, wer Informationen über dein Profil sehen kann.",
|
||||
"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.",
|
||||
"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",
|
||||
"privacyVisibilityOnlyContacts": "Nur Kontakte",
|
||||
"privacyVisibilityEveryone": "Alle",
|
||||
|
|
@ -1178,5 +1182,20 @@
|
|||
"notificationSticker": "hat einen Sticker gesendet{inGroup}.",
|
||||
"storeSticker": "Sticker speichern",
|
||||
"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)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,10 @@
|
|||
"publicProfilePrivacyDescription": "Choose who can see information about your profile.",
|
||||
"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.",
|
||||
"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",
|
||||
"privacyVisibilityOnlyContacts": "Only Contacts",
|
||||
"privacyVisibilityEveryone": "Everyone",
|
||||
|
|
@ -1188,5 +1192,20 @@
|
|||
"notificationSticker": "sent a sticker{inGroup}.",
|
||||
"storeSticker": "Save sticker",
|
||||
"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)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ class ApiService {
|
|||
rust_api.RustApi.reuploadPendingMedia(),
|
||||
'reuploadPendingMedia',
|
||||
);
|
||||
unawaitedRustCall(
|
||||
rust_api.RustApi.refreshContactJoinedDates(),
|
||||
'refreshContactJoinedDates',
|
||||
);
|
||||
|
||||
twonlyDB.markUpdated();
|
||||
// resetUserDiscoveryRequestUpdates();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
String localizedReason, {
|
||||
bool force = true,
|
||||
|
|
|
|||
312
lib/src/visual/components/release_notes.comp.dart
Normal file
312
lib/src/visual/components/release_notes.comp.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/src/visual/release_notes/release_notes_0_6_0.dart
Normal file
43
lib/src/visual/release_notes/release_notes_0_6_0.dart
Normal 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,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -136,6 +136,14 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
|
|||
requested: const Value(false),
|
||||
blocked: 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(
|
||||
|
|
|
|||
|
|
@ -170,6 +170,14 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
joinedAt: userdata.joinedAt == null
|
||||
? const Value.absent()
|
||||
: Value(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
userdata.joinedAt! * Duration.millisecondsPerSecond,
|
||||
isUtc: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,14 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
joinedAt: userdata.joinedAt == null
|
||||
? const Value.absent()
|
||||
: Value(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
userdata.joinedAt! * Duration.millisecondsPerSecond,
|
||||
isUtc: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@ class FriendSuggestionsComp extends StatelessWidget {
|
|||
requested: const Value(false),
|
||||
blocked: const Value(false),
|
||||
deletedByUser: const Value(false),
|
||||
joinedAt: userdata.joinedAt == null
|
||||
? const Value.absent()
|
||||
: Value(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
userdata.joinedAt! * Duration.millisecondsPerSecond,
|
||||
isUtc: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -226,11 +226,18 @@ class _ContactViewState extends State<ContactView> {
|
|||
),
|
||||
if (getContactDisplayName(contact) != contact.username)
|
||||
Center(child: Text('(${contact.username})')),
|
||||
if (contact.twonlyScore != null)
|
||||
if (contact.twonlyScore != null ||
|
||||
(contact.joinedAt != null && !contact.accountDeleted))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: Center(
|
||||
child: IntrinsicWidth(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (contact.twonlyScore != null)
|
||||
IntrinsicWidth(
|
||||
child: MyChip(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -243,7 +250,9 @@ class _ContactViewState extends State<ContactView> {
|
|||
onTap: () => showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.lang.publicProfileTwonlyScore),
|
||||
title: Text(
|
||||
context.lang.publicProfileTwonlyScore,
|
||||
),
|
||||
content: Text(
|
||||
context.lang.contactTwonlyScoreDescription,
|
||||
),
|
||||
|
|
@ -257,6 +266,25 @@ class _ContactViewState extends State<ContactView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
|
|
|
|||
|
|
@ -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/utils/log.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_controller_view.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.
|
||||
bool _editorOpen = false;
|
||||
bool _startPreloading = false;
|
||||
bool _startupChecksComplete = false;
|
||||
bool _releaseNotesCheckScheduled = false;
|
||||
|
||||
final MainCameraController _mainCameraController = MainCameraController();
|
||||
late final PageController _homeViewPageController;
|
||||
|
|
@ -67,6 +71,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
routerProvider.routerDelegate.addListener(_onRouteChanged);
|
||||
var initialPage = widget.initialPage;
|
||||
if (HomeViewState.pendingSharedLink != null) {
|
||||
initialPage = 1;
|
||||
|
|
@ -97,6 +102,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
if (index != 1) {
|
||||
unawaited(_mainCameraController.closeCamera());
|
||||
}
|
||||
_scheduleReleaseNotesIfNeeded();
|
||||
});
|
||||
|
||||
_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((_) {
|
||||
if (_mainCameraController.sharedLinkForPreview == null &&
|
||||
|
|
@ -293,6 +304,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
routerProvider.routerDelegate.removeListener(_onRouteChanged);
|
||||
_onMessageOpenedAppSub?.cancel();
|
||||
_homeViewPageIndexSub?.cancel();
|
||||
_selectNotificationSub?.cancel();
|
||||
|
|
@ -310,10 +322,29 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
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
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_scheduleReleaseNotesIfNeeded();
|
||||
if (AppState.hasCameraPermissions &&
|
||||
_offsetRatio < 1 &&
|
||||
!_mainCameraController.isSharePreviewIsShown &&
|
||||
|
|
@ -378,6 +409,9 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
_activePageIdx = pageIndex;
|
||||
}
|
||||
});
|
||||
if (notification is ScrollEndNotification) {
|
||||
_scheduleReleaseNotesIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
if (AppState.hasCameraPermissions &&
|
||||
|
|
@ -505,7 +539,10 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.bounceIn,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
_scheduleReleaseNotesIfNeeded();
|
||||
}
|
||||
},
|
||||
currentIndex: _activePageIdx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -332,6 +332,17 @@
|
|||
"name": "twonly_score"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "joined_at",
|
||||
"ordinal": 30,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "joined_at"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
|
@ -367,6 +378,7 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -146,6 +146,13 @@ pub(crate) async fn handle_contact_request(
|
|||
.build()
|
||||
.insert_on_conflict_update(tr)
|
||||
.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 => {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ message Response {
|
|||
optional int64 signed_prekey_id = 6;
|
||||
optional int64 registration_id = 8;
|
||||
optional PqcBundle pqc_bundle = 9;
|
||||
optional int64 joined_at = 10;
|
||||
}
|
||||
|
||||
message UploadToken {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
let database = ctx.app_db.read().await.clone();
|
||||
let contacts = sqlx::query_scalar!(
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ pub struct FrbUserData {
|
|||
pub user_id: i64,
|
||||
pub username: Vec<u8>,
|
||||
pub public_identity_key: Vec<u8>,
|
||||
pub joined_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -593,8 +594,13 @@ impl RustApi {
|
|||
user_id: res.user_id,
|
||||
username: res.username.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<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::check_for_deleted_usernames(ctx).await
|
||||
|
|
@ -614,6 +620,7 @@ impl RustApi {
|
|||
user_id: res.user_id,
|
||||
username: res.username.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> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -16,7 +16,7 @@ mod legacy_import;
|
|||
pub mod tables;
|
||||
|
||||
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
|
||||
/// tables are deliberately absent because they are reconstructed locally.
|
||||
|
|
@ -402,6 +402,34 @@ mod change_notification_tests {
|
|||
(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 {
|
||||
format!("INSERT INTO app_metadata(key, value) VALUES('{key}', '1')")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub struct Contact {
|
|||
pub widget_sharing_allowed: i64,
|
||||
pub widget_sharing_granted: i64,
|
||||
pub twonly_score: Option<i64>,
|
||||
pub joined_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(bon::Builder)]
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1844664049;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1901082359;
|
||||
|
||||
// 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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
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_username = <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 {
|
||||
user_id: var_userId,
|
||||
username: var_username,
|
||||
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_lastChangeLogHash = <Option<Vec<u8>>>::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_hideWidgetShareExplainer = <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,
|
||||
last_change_log_hash: var_lastChangeLogHash,
|
||||
hide_change_log: var_hideChangeLog,
|
||||
last_release_notes_version: var_lastReleaseNotesVersion,
|
||||
hide_memories_backup_promo: var_hideMemoriesBackupPromo,
|
||||
hide_widget_share_explainer: var_hideWidgetShareExplainer,
|
||||
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),
|
||||
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),
|
||||
94 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
|
||||
95 => wire__crate__bridge__api__rust_api_register_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),
|
||||
97 => wire__crate__bridge__api__rust_api_register_passwordless_notification_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),
|
||||
99 => wire__crate__bridge__api__rust_api_reload_configuration_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),
|
||||
101 => wire__crate__bridge__api__rust_api_remove_media_files_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),
|
||||
103 => wire__crate__bridge__api__rust_api_request_binary_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),
|
||||
105 => wire__crate__bridge__api__rust_api_request_media_reupload_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),
|
||||
107 => wire__crate__bridge__api__rust_api_retransmit_all_messages_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),
|
||||
109 => wire__crate__bridge__api__rust_api_reupload_pending_media_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),
|
||||
111 => wire__crate__bridge__api__rust_api_send_binary_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),
|
||||
113 => wire__crate__bridge__api__rust_api_send_encrypted_content_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),
|
||||
115 => wire__crate__bridge__api__rust_api_send_media_to_groups_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),
|
||||
117 => wire__crate__bridge__api__rust_api_send_text_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),
|
||||
119 => wire__crate__bridge__api__rust_api_set_background_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),
|
||||
121 => wire__crate__bridge__api__rust_api_set_login_token_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),
|
||||
123 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_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),
|
||||
125 => wire__crate__bridge__api__rust_api_set_network_available_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),
|
||||
127 => wire__crate__bridge__api__rust_api_submit_recovery_share_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),
|
||||
129 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_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),
|
||||
131 => wire__crate__bridge__api__rust_api_unregister_home_widget_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),
|
||||
133 => wire__crate__bridge__api__rust_api_update_signed_pre_key_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),
|
||||
135 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_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),
|
||||
137 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_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),
|
||||
139 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_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),
|
||||
141 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_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),
|
||||
143 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_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),
|
||||
145 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_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),
|
||||
147 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_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),
|
||||
149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_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),
|
||||
151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_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),
|
||||
153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_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),
|
||||
155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_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),
|
||||
157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_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),
|
||||
159 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_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),
|
||||
161 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_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),
|
||||
163 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_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),
|
||||
165 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_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),
|
||||
167 => wire__crate__bridge__wrapper__rust_utils_generate_shares_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),
|
||||
169 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
170 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
171 => wire__crate__bridge__stories__story_audience_chats_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),
|
||||
173 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||
174 => wire__crate__bridge__webxdc__updates_after_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),
|
||||
177 => wire__crate__bridge__user_config__user_config_api_import_json_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),
|
||||
179 => wire__crate__bridge__user_config__user_config_api_save_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),
|
||||
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_refresh_widget_manifest_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_home_widget_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_register_passwordless_recovery_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_additional_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_report_user_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_contact_by_username_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_request_memories_upload_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_retry_pending_media_reuploads_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_save_media_to_gallery_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_contact_profile_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_encrypted_content_to_group_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_queued_message_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_send_typing_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_home_widget_groups_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_display_limit_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_media_trim_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_store_media_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_sync_widget_permissions_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_try_request_contact_by_id_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_fcm_token_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__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_changes_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_legacy_import_complete_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__app_database__rust_app_database_select_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_get_backup_download_token_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_id_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_get_identity_backup_bytes_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_restore_identity_backup_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_decrypt_cloud_media_key_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_signal_identity_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_serialized_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_remove_local_credentials_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__key_manager__rust_key_manager_set_user_id_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_encrypt_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_generate_pqc_prekeys_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_safety_number_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_process_prekey_bundle_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_generate_shares_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__send_update_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__stories__story_audience_chats_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__groups__update_group_name_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_create_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_load_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!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -8350,12 +8391,12 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||
55 => {
|
||||
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,
|
||||
rust_vec_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!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -8720,6 +8761,7 @@ impl flutter_rust_bridge::IntoDart for crate::bridge::api::FrbUserData {
|
|||
self.user_id.into_into_dart().into_dart(),
|
||||
self.username.into_into_dart().into_dart(),
|
||||
self.public_identity_key.into_into_dart().into_dart(),
|
||||
self.joined_at.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
|
|
@ -9321,6 +9363,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig {
|
|||
.into_dart(),
|
||||
self.last_change_log_hash.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_widget_share_explainer
|
||||
.into_into_dart()
|
||||
|
|
@ -9758,6 +9801,7 @@ impl SseEncode for crate::bridge::api::FrbUserData {
|
|||
<i64>::sse_encode(self.user_id, serializer);
|
||||
<Vec<u8>>::sse_encode(self.username, 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);
|
||||
<Option<Vec<u8>>>::sse_encode(self.last_change_log_hash, 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_widget_share_explainer, serializer);
|
||||
<bool>::sse_encode(self.update_fcm_token, serializer);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ impl ContactService {
|
|||
.build()
|
||||
.insert_on_conflict_update(&mut transaction)
|
||||
.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?;
|
||||
|
||||
self.send_contact_request(
|
||||
|
|
@ -113,10 +120,12 @@ impl ContactService {
|
|||
let database = self.ctx.app_db.read().await.clone();
|
||||
// The server answered with a bundle, so a previous `UserIdNotFound`
|
||||
// (or a manual mark) must not keep the contact blocked.
|
||||
sqlx::query!(
|
||||
"UPDATE contacts SET signal_version = 'v2', account_deleted = 0 WHERE user_id = ?",
|
||||
user_id
|
||||
sqlx::query(
|
||||
"UPDATE contacts SET signal_version = 'v2', account_deleted = 0, \
|
||||
joined_at = COALESCE(?, joined_at) WHERE user_id = ?",
|
||||
)
|
||||
.bind(user.joined_at)
|
||||
.bind(user_id)
|
||||
.execute(&database.pool)
|
||||
.await?;
|
||||
release_deferred_receipts(&database, user_id).await?;
|
||||
|
|
|
|||
|
|
@ -361,6 +361,10 @@ pub struct UserConfig {
|
|||
#[serde(default = "defaults::true_value")]
|
||||
#[frb(non_final)]
|
||||
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)]
|
||||
#[frb(non_final)]
|
||||
pub hide_memories_backup_promo: bool,
|
||||
|
|
@ -551,6 +555,7 @@ mod tests {
|
|||
);
|
||||
assert!(!config.is_user_discovery_enabled);
|
||||
assert_eq!(config.last_server_message_at, None);
|
||||
assert_eq!(config.last_release_notes_version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
57
test/visual/components/release_notes_test.dart
Normal file
57
test/visual/components/release_notes_test.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
48
test/visual/contact_joined_at_test.dart
Normal file
48
test/visual/contact_joined_at_test.dart
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue