improve games

This commit is contained in:
otsmr 2026-09-09 20:26:26 +02:00
parent 57fa71af0c
commit 3e321ed53d
53 changed files with 3500 additions and 596 deletions

View file

@ -12,11 +12,25 @@ Future<void> refreshCatalog() =>
/// What the in-app store offers, newest version of each app only. /// What the in-app store offers, newest version of each app only.
/// ///
/// `languages` is what the UI prefers, most preferred first. Descriptions are /// `languages` is what the UI prefers, most preferred first. Names and
/// cached in every language the catalog carries and picked here, so which /// descriptions are cached in every language the catalog carries and picked
/// language a user reads is never sent anywhere. /// here, so which language a user reads is never sent anywhere.
Future<List<WebxdcStoreApp>> catalog({required List<String> languages}) => Future<List<WebxdcStoreApp>> catalog({
RustLib.instance.api.crateBridgeWebxdcCatalog(languages: languages); required String groupId,
required List<String> languages,
}) => RustLib.instance.api.crateBridgeWebxdcCatalog(
groupId: groupId,
languages: languages,
);
/// One-time apps already placed into a chat, for the profile shortcut.
Future<List<WebxdcOneTimeInstance>> oneTimeInstances({
required String groupId,
required List<String> languages,
}) => RustLib.instance.api.crateBridgeWebxdcOneTimeInstances(
groupId: groupId,
languages: languages,
);
/// Places an app into a chat and returns the id of the message that carries it. /// Places an app into a chat and returns the id of the message that carries it.
Future<String> createInstance({ Future<String> createInstance({
@ -32,12 +46,17 @@ Future<String> createInstance({
Future<WebxdcInstanceInfo?> instance({required String instanceId}) => Future<WebxdcInstanceInfo?> instance({required String instanceId}) =>
RustLib.instance.api.crateBridgeWebxdcInstance(instanceId: instanceId); RustLib.instance.api.crateBridgeWebxdcInstance(instanceId: instanceId);
/// Downloads and verifies the bundle if it is not already on disk, and returns /// Opens cached code, adopting an update only if already downloaded.
/// where it landed. Called when the user starts an app, never on message /// Downloads and verifies the pinned bundle only when it is missing. Called when the user starts an app, never on message
/// arrival: a message must not be able to make a device fetch anything. /// arrival: a message must not be able to make a device fetch anything.
Future<String> prepareBundle({required String instanceId}) => Future<String> prepareBundle({required String instanceId}) =>
RustLib.instance.api.crateBridgeWebxdcPrepareBundle(instanceId: instanceId); RustLib.instance.api.crateBridgeWebxdcPrepareBundle(instanceId: instanceId);
/// Background update check after a webxdc app closes. Downloads only; the
/// running version changes on a later prepare_bundle call, never mid-session.
Future<void> cacheUpdate({required String instanceId}) =>
RustLib.instance.api.crateBridgeWebxdcCacheUpdate(instanceId: instanceId);
/// Answers one request the webview made for a file inside the bundle. /// Answers one request the webview made for a file inside the bundle.
/// ///
/// The platform shim has already established that the request came from this /// The platform shim has already established that the request came from this
@ -65,6 +84,7 @@ Future<void> sendUpdate({
String? href, String? href,
String? summary, String? summary,
String? document, String? document,
String? notify,
}) => RustLib.instance.api.crateBridgeWebxdcSendUpdate( }) => RustLib.instance.api.crateBridgeWebxdcSendUpdate(
instanceId: instanceId, instanceId: instanceId,
payload: payload, payload: payload,
@ -72,6 +92,7 @@ Future<void> sendUpdate({
href: href, href: href,
summary: summary, summary: summary,
document: document, document: document,
notify: notify,
); );
/// The address the running app sees for a participant. Stable inside one /// The address the running app sees for a participant. Stable inside one
@ -93,6 +114,10 @@ Future<String?> deleteInstance({required String instanceId}) => RustLib
.api .api
.crateBridgeWebxdcDeleteInstance(instanceId: instanceId); .crateBridgeWebxdcDeleteInstance(instanceId: instanceId);
/// Current members of the instance's chat; no account identifiers leave Rust.
Future<String> members({required String instanceId}) =>
RustLib.instance.api.crateBridgeWebxdcMembers(instanceId: instanceId);
class WebxdcInstanceInfo { class WebxdcInstanceInfo {
final String instanceId; final String instanceId;
final String groupId; final String groupId;
@ -140,6 +165,41 @@ class WebxdcInstanceInfo {
document == other.document; document == other.document;
} }
class WebxdcOneTimeInstance {
final String instanceId;
final String name;
final Uint8List? icon;
final String? summary;
final String? document;
const WebxdcOneTimeInstance({
required this.instanceId,
required this.name,
this.icon,
this.summary,
this.document,
});
@override
int get hashCode =>
instanceId.hashCode ^
name.hashCode ^
icon.hashCode ^
summary.hashCode ^
document.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is WebxdcOneTimeInstance &&
runtimeType == other.runtimeType &&
instanceId == other.instanceId &&
name == other.name &&
icon == other.icon &&
summary == other.summary &&
document == other.document;
}
class WebxdcResponse { class WebxdcResponse {
final int status; final int status;
final String mime; final String mime;
@ -187,6 +247,11 @@ class WebxdcStoreApp {
final String? sourceCodeUrl; final String? sourceCodeUrl;
final Uint8List? icon; final Uint8List? icon;
final PlatformInt64 bundleBytes; final PlatformInt64 bundleBytes;
final bool proOnly;
final bool oneTime;
/// The existing instance in this chat when a one-time app was already placed.
final String? instanceId;
const WebxdcStoreApp({ const WebxdcStoreApp({
required this.appId, required this.appId,
@ -196,6 +261,9 @@ class WebxdcStoreApp {
this.sourceCodeUrl, this.sourceCodeUrl,
this.icon, this.icon,
required this.bundleBytes, required this.bundleBytes,
required this.proOnly,
required this.oneTime,
this.instanceId,
}); });
@override @override
@ -206,7 +274,10 @@ class WebxdcStoreApp {
description.hashCode ^ description.hashCode ^
sourceCodeUrl.hashCode ^ sourceCodeUrl.hashCode ^
icon.hashCode ^ icon.hashCode ^
bundleBytes.hashCode; bundleBytes.hashCode ^
proOnly.hashCode ^
oneTime.hashCode ^
instanceId.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -219,7 +290,10 @@ class WebxdcStoreApp {
description == other.description && description == other.description &&
sourceCodeUrl == other.sourceCodeUrl && sourceCodeUrl == other.sourceCodeUrl &&
icon == other.icon && icon == other.icon &&
bundleBytes == other.bundleBytes; bundleBytes == other.bundleBytes &&
proOnly == other.proOnly &&
oneTime == other.oneTime &&
instanceId == other.instanceId;
} }
class WebxdcUpdateEntry { class WebxdcUpdateEntry {

File diff suppressed because it is too large Load diff

View file

@ -238,6 +238,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<SqlValue> dco_decode_list_sql_value(dynamic raw); List<SqlValue> dco_decode_list_sql_value(dynamic raw);
@protected
List<WebxdcOneTimeInstance> dco_decode_list_webxdc_one_time_instance(
dynamic raw,
);
@protected @protected
List<WebxdcStoreApp> dco_decode_list_webxdc_store_app(dynamic raw); List<WebxdcStoreApp> dco_decode_list_webxdc_store_app(dynamic raw);
@ -393,6 +398,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw); WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw);
@protected
WebxdcOneTimeInstance dco_decode_webxdc_one_time_instance(dynamic raw);
@protected @protected
WebxdcResponse dco_decode_webxdc_response(dynamic raw); WebxdcResponse dco_decode_webxdc_response(dynamic raw);
@ -641,6 +649,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer); List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
@protected
List<WebxdcOneTimeInstance> sse_decode_list_webxdc_one_time_instance(
SseDeserializer deserializer,
);
@protected @protected
List<WebxdcStoreApp> sse_decode_list_webxdc_store_app( List<WebxdcStoreApp> sse_decode_list_webxdc_store_app(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -820,6 +833,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
WebxdcOneTimeInstance sse_decode_webxdc_one_time_instance(
SseDeserializer deserializer,
);
@protected @protected
WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer); WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer);
@ -1143,6 +1161,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer); void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
@protected
void sse_encode_list_webxdc_one_time_instance(
List<WebxdcOneTimeInstance> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_list_webxdc_store_app( void sse_encode_list_webxdc_store_app(
List<WebxdcStoreApp> self, List<WebxdcStoreApp> self,
@ -1362,6 +1386,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_webxdc_one_time_instance(
WebxdcOneTimeInstance self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_webxdc_response( void sse_encode_webxdc_response(
WebxdcResponse self, WebxdcResponse self,

View file

@ -240,6 +240,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<SqlValue> dco_decode_list_sql_value(dynamic raw); List<SqlValue> dco_decode_list_sql_value(dynamic raw);
@protected
List<WebxdcOneTimeInstance> dco_decode_list_webxdc_one_time_instance(
dynamic raw,
);
@protected @protected
List<WebxdcStoreApp> dco_decode_list_webxdc_store_app(dynamic raw); List<WebxdcStoreApp> dco_decode_list_webxdc_store_app(dynamic raw);
@ -395,6 +400,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw); WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw);
@protected
WebxdcOneTimeInstance dco_decode_webxdc_one_time_instance(dynamic raw);
@protected @protected
WebxdcResponse dco_decode_webxdc_response(dynamic raw); WebxdcResponse dco_decode_webxdc_response(dynamic raw);
@ -643,6 +651,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer); List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
@protected
List<WebxdcOneTimeInstance> sse_decode_list_webxdc_one_time_instance(
SseDeserializer deserializer,
);
@protected @protected
List<WebxdcStoreApp> sse_decode_list_webxdc_store_app( List<WebxdcStoreApp> sse_decode_list_webxdc_store_app(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -822,6 +835,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
WebxdcOneTimeInstance sse_decode_webxdc_one_time_instance(
SseDeserializer deserializer,
);
@protected @protected
WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer); WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer);
@ -1145,6 +1163,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer); void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
@protected
void sse_encode_list_webxdc_one_time_instance(
List<WebxdcOneTimeInstance> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_list_webxdc_store_app( void sse_encode_list_webxdc_store_app(
List<WebxdcStoreApp> self, List<WebxdcStoreApp> self,
@ -1364,6 +1388,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_webxdc_one_time_instance(
WebxdcOneTimeInstance self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_webxdc_response( void sse_encode_webxdc_response(
WebxdcResponse self, WebxdcResponse self,

View file

@ -641,8 +641,34 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
} }
Future<void> deleteMessagesByGroupId(String groupId) { Future<void> deleteMessagesByGroupId(String groupId) async {
return (delete(messages)..where((t) => t.groupId.equals(groupId))).go(); await transaction(() async {
// A one-time app is the chat's durable shared object. Keep its card (and
// therefore its instance and update log) while removing the visible chat
// history and every ordinary app.
await customStatement(
'''
DELETE FROM messages
WHERE group_id = ?
AND NOT EXISTS (
SELECT 1
FROM webxdc_instances AS instance
LEFT JOIN webxdc_apps AS app
ON app.app_id = instance.app_id
AND app.version = instance.version
WHERE instance.instance_id = messages.message_id
AND (app.one_time = 1 OR app.app_id IS NULL)
)''',
[groupId],
);
await customStatement(
'''
UPDATE groups
SET deleted_content = 1, archived = 0, pinned = 0, draft_message = NULL
WHERE group_id = ?''',
[groupId],
);
});
} }
SingleOrNullSelectable<Message> getMessageById(String messageId) { SingleOrNullSelectable<Message> getMessageById(String messageId) {

View file

@ -31,6 +31,9 @@ class WebxdcApps extends Table {
IntColumn get bundleBytes => integer()(); IntColumn get bundleBytes => integer()();
BoolColumn get published => boolean().withDefault(const Constant(true))(); BoolColumn get published => boolean().withDefault(const Constant(true))();
IntColumn get cachedAt => integer()(); IntColumn get cachedAt => integer()();
BoolColumn get proOnly => boolean().withDefault(const Constant(false))();
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
BoolColumn get oneTime => boolean().withDefault(const Constant(false))();
@override @override
Set<Column> get primaryKey => {appId, version}; Set<Column> get primaryKey => {appId, version};

View file

@ -12663,6 +12663,48 @@ class $WebxdcAppsTable extends WebxdcApps
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: true, requiredDuringInsert: true,
); );
static const VerificationMeta _proOnlyMeta = const VerificationMeta(
'proOnly',
);
@override
late final GeneratedColumn<bool> proOnly = GeneratedColumn<bool>(
'pro_only',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("pro_only" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _sortOrderMeta = const VerificationMeta(
'sortOrder',
);
@override
late final GeneratedColumn<int> sortOrder = GeneratedColumn<int>(
'sort_order',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0),
);
static const VerificationMeta _oneTimeMeta = const VerificationMeta(
'oneTime',
);
@override
late final GeneratedColumn<bool> oneTime = GeneratedColumn<bool>(
'one_time',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("one_time" IN (0, 1))',
),
defaultValue: const Constant(false),
);
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
appId, appId,
@ -12676,6 +12718,9 @@ class $WebxdcAppsTable extends WebxdcApps
bundleBytes, bundleBytes,
published, published,
cachedAt, cachedAt,
proOnly,
sortOrder,
oneTime,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@ -12782,6 +12827,24 @@ class $WebxdcAppsTable extends WebxdcApps
} else if (isInserting) { } else if (isInserting) {
context.missing(_cachedAtMeta); context.missing(_cachedAtMeta);
} }
if (data.containsKey('pro_only')) {
context.handle(
_proOnlyMeta,
proOnly.isAcceptableOrUnknown(data['pro_only']!, _proOnlyMeta),
);
}
if (data.containsKey('sort_order')) {
context.handle(
_sortOrderMeta,
sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta),
);
}
if (data.containsKey('one_time')) {
context.handle(
_oneTimeMeta,
oneTime.isAcceptableOrUnknown(data['one_time']!, _oneTimeMeta),
);
}
return context; return context;
} }
@ -12835,6 +12898,18 @@ class $WebxdcAppsTable extends WebxdcApps
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}cached_at'], data['${effectivePrefix}cached_at'],
)!, )!,
proOnly: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}pro_only'],
)!,
sortOrder: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}sort_order'],
)!,
oneTime: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}one_time'],
)!,
); );
} }
@ -12866,6 +12941,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
final int bundleBytes; final int bundleBytes;
final bool published; final bool published;
final int cachedAt; final int cachedAt;
final bool proOnly;
final int sortOrder;
final bool oneTime;
const WebxdcApp({ const WebxdcApp({
required this.appId, required this.appId,
required this.version, required this.version,
@ -12878,6 +12956,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
required this.bundleBytes, required this.bundleBytes,
required this.published, required this.published,
required this.cachedAt, required this.cachedAt,
required this.proOnly,
required this.sortOrder,
required this.oneTime,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@ -12897,6 +12978,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
map['bundle_bytes'] = Variable<int>(bundleBytes); map['bundle_bytes'] = Variable<int>(bundleBytes);
map['published'] = Variable<bool>(published); map['published'] = Variable<bool>(published);
map['cached_at'] = Variable<int>(cachedAt); map['cached_at'] = Variable<int>(cachedAt);
map['pro_only'] = Variable<bool>(proOnly);
map['sort_order'] = Variable<int>(sortOrder);
map['one_time'] = Variable<bool>(oneTime);
return map; return map;
} }
@ -12915,6 +12999,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
bundleBytes: Value(bundleBytes), bundleBytes: Value(bundleBytes),
published: Value(published), published: Value(published),
cachedAt: Value(cachedAt), cachedAt: Value(cachedAt),
proOnly: Value(proOnly),
sortOrder: Value(sortOrder),
oneTime: Value(oneTime),
); );
} }
@ -12935,6 +13022,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
bundleBytes: serializer.fromJson<int>(json['bundleBytes']), bundleBytes: serializer.fromJson<int>(json['bundleBytes']),
published: serializer.fromJson<bool>(json['published']), published: serializer.fromJson<bool>(json['published']),
cachedAt: serializer.fromJson<int>(json['cachedAt']), cachedAt: serializer.fromJson<int>(json['cachedAt']),
proOnly: serializer.fromJson<bool>(json['proOnly']),
sortOrder: serializer.fromJson<int>(json['sortOrder']),
oneTime: serializer.fromJson<bool>(json['oneTime']),
); );
} }
@override @override
@ -12952,6 +13042,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
'bundleBytes': serializer.toJson<int>(bundleBytes), 'bundleBytes': serializer.toJson<int>(bundleBytes),
'published': serializer.toJson<bool>(published), 'published': serializer.toJson<bool>(published),
'cachedAt': serializer.toJson<int>(cachedAt), 'cachedAt': serializer.toJson<int>(cachedAt),
'proOnly': serializer.toJson<bool>(proOnly),
'sortOrder': serializer.toJson<int>(sortOrder),
'oneTime': serializer.toJson<bool>(oneTime),
}; };
} }
@ -12967,6 +13060,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
int? bundleBytes, int? bundleBytes,
bool? published, bool? published,
int? cachedAt, int? cachedAt,
bool? proOnly,
int? sortOrder,
bool? oneTime,
}) => WebxdcApp( }) => WebxdcApp(
appId: appId ?? this.appId, appId: appId ?? this.appId,
version: version ?? this.version, version: version ?? this.version,
@ -12981,6 +13077,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
bundleBytes: bundleBytes ?? this.bundleBytes, bundleBytes: bundleBytes ?? this.bundleBytes,
published: published ?? this.published, published: published ?? this.published,
cachedAt: cachedAt ?? this.cachedAt, cachedAt: cachedAt ?? this.cachedAt,
proOnly: proOnly ?? this.proOnly,
sortOrder: sortOrder ?? this.sortOrder,
oneTime: oneTime ?? this.oneTime,
); );
WebxdcApp copyWithCompanion(WebxdcAppsCompanion data) { WebxdcApp copyWithCompanion(WebxdcAppsCompanion data) {
return WebxdcApp( return WebxdcApp(
@ -13005,6 +13104,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
: this.bundleBytes, : this.bundleBytes,
published: data.published.present ? data.published.value : this.published, published: data.published.present ? data.published.value : this.published,
cachedAt: data.cachedAt.present ? data.cachedAt.value : this.cachedAt, cachedAt: data.cachedAt.present ? data.cachedAt.value : this.cachedAt,
proOnly: data.proOnly.present ? data.proOnly.value : this.proOnly,
sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder,
oneTime: data.oneTime.present ? data.oneTime.value : this.oneTime,
); );
} }
@ -13021,7 +13123,10 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
..write('bundleSha256: $bundleSha256, ') ..write('bundleSha256: $bundleSha256, ')
..write('bundleBytes: $bundleBytes, ') ..write('bundleBytes: $bundleBytes, ')
..write('published: $published, ') ..write('published: $published, ')
..write('cachedAt: $cachedAt') ..write('cachedAt: $cachedAt, ')
..write('proOnly: $proOnly, ')
..write('sortOrder: $sortOrder, ')
..write('oneTime: $oneTime')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -13039,6 +13144,9 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
bundleBytes, bundleBytes,
published, published,
cachedAt, cachedAt,
proOnly,
sortOrder,
oneTime,
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -13054,7 +13162,10 @@ class WebxdcApp extends DataClass implements Insertable<WebxdcApp> {
other.bundleSha256 == this.bundleSha256 && other.bundleSha256 == this.bundleSha256 &&
other.bundleBytes == this.bundleBytes && other.bundleBytes == this.bundleBytes &&
other.published == this.published && other.published == this.published &&
other.cachedAt == this.cachedAt); other.cachedAt == this.cachedAt &&
other.proOnly == this.proOnly &&
other.sortOrder == this.sortOrder &&
other.oneTime == this.oneTime);
} }
class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> { class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
@ -13069,6 +13180,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
final Value<int> bundleBytes; final Value<int> bundleBytes;
final Value<bool> published; final Value<bool> published;
final Value<int> cachedAt; final Value<int> cachedAt;
final Value<bool> proOnly;
final Value<int> sortOrder;
final Value<bool> oneTime;
final Value<int> rowid; final Value<int> rowid;
const WebxdcAppsCompanion({ const WebxdcAppsCompanion({
this.appId = const Value.absent(), this.appId = const Value.absent(),
@ -13082,6 +13196,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
this.bundleBytes = const Value.absent(), this.bundleBytes = const Value.absent(),
this.published = const Value.absent(), this.published = const Value.absent(),
this.cachedAt = const Value.absent(), this.cachedAt = const Value.absent(),
this.proOnly = const Value.absent(),
this.sortOrder = const Value.absent(),
this.oneTime = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
WebxdcAppsCompanion.insert({ WebxdcAppsCompanion.insert({
@ -13096,6 +13213,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
required int bundleBytes, required int bundleBytes,
this.published = const Value.absent(), this.published = const Value.absent(),
required int cachedAt, required int cachedAt,
this.proOnly = const Value.absent(),
this.sortOrder = const Value.absent(),
this.oneTime = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : appId = Value(appId), }) : appId = Value(appId),
version = Value(version), version = Value(version),
@ -13115,6 +13235,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
Expression<int>? bundleBytes, Expression<int>? bundleBytes,
Expression<bool>? published, Expression<bool>? published,
Expression<int>? cachedAt, Expression<int>? cachedAt,
Expression<bool>? proOnly,
Expression<int>? sortOrder,
Expression<bool>? oneTime,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
@ -13129,6 +13252,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
if (bundleBytes != null) 'bundle_bytes': bundleBytes, if (bundleBytes != null) 'bundle_bytes': bundleBytes,
if (published != null) 'published': published, if (published != null) 'published': published,
if (cachedAt != null) 'cached_at': cachedAt, if (cachedAt != null) 'cached_at': cachedAt,
if (proOnly != null) 'pro_only': proOnly,
if (sortOrder != null) 'sort_order': sortOrder,
if (oneTime != null) 'one_time': oneTime,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@ -13145,6 +13271,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
Value<int>? bundleBytes, Value<int>? bundleBytes,
Value<bool>? published, Value<bool>? published,
Value<int>? cachedAt, Value<int>? cachedAt,
Value<bool>? proOnly,
Value<int>? sortOrder,
Value<bool>? oneTime,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return WebxdcAppsCompanion( return WebxdcAppsCompanion(
@ -13159,6 +13288,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
bundleBytes: bundleBytes ?? this.bundleBytes, bundleBytes: bundleBytes ?? this.bundleBytes,
published: published ?? this.published, published: published ?? this.published,
cachedAt: cachedAt ?? this.cachedAt, cachedAt: cachedAt ?? this.cachedAt,
proOnly: proOnly ?? this.proOnly,
sortOrder: sortOrder ?? this.sortOrder,
oneTime: oneTime ?? this.oneTime,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@ -13199,6 +13331,15 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
if (cachedAt.present) { if (cachedAt.present) {
map['cached_at'] = Variable<int>(cachedAt.value); map['cached_at'] = Variable<int>(cachedAt.value);
} }
if (proOnly.present) {
map['pro_only'] = Variable<bool>(proOnly.value);
}
if (sortOrder.present) {
map['sort_order'] = Variable<int>(sortOrder.value);
}
if (oneTime.present) {
map['one_time'] = Variable<bool>(oneTime.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@ -13219,6 +13360,9 @@ class WebxdcAppsCompanion extends UpdateCompanion<WebxdcApp> {
..write('bundleBytes: $bundleBytes, ') ..write('bundleBytes: $bundleBytes, ')
..write('published: $published, ') ..write('published: $published, ')
..write('cachedAt: $cachedAt, ') ..write('cachedAt: $cachedAt, ')
..write('proOnly: $proOnly, ')
..write('sortOrder: $sortOrder, ')
..write('oneTime: $oneTime, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@ -25803,6 +25947,9 @@ typedef $$WebxdcAppsTableCreateCompanionBuilder =
required int bundleBytes, required int bundleBytes,
Value<bool> published, Value<bool> published,
required int cachedAt, required int cachedAt,
Value<bool> proOnly,
Value<int> sortOrder,
Value<bool> oneTime,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$WebxdcAppsTableUpdateCompanionBuilder = typedef $$WebxdcAppsTableUpdateCompanionBuilder =
@ -25818,6 +25965,9 @@ typedef $$WebxdcAppsTableUpdateCompanionBuilder =
Value<int> bundleBytes, Value<int> bundleBytes,
Value<bool> published, Value<bool> published,
Value<int> cachedAt, Value<int> cachedAt,
Value<bool> proOnly,
Value<int> sortOrder,
Value<bool> oneTime,
Value<int> rowid, Value<int> rowid,
}); });
@ -25884,6 +26034,21 @@ class $$WebxdcAppsTableFilterComposer
column: $table.cachedAt, column: $table.cachedAt,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get proOnly => $composableBuilder(
column: $table.proOnly,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get sortOrder => $composableBuilder(
column: $table.sortOrder,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get oneTime => $composableBuilder(
column: $table.oneTime,
builder: (column) => ColumnFilters(column),
);
} }
class $$WebxdcAppsTableOrderingComposer class $$WebxdcAppsTableOrderingComposer
@ -25949,6 +26114,21 @@ class $$WebxdcAppsTableOrderingComposer
column: $table.cachedAt, column: $table.cachedAt,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get proOnly => $composableBuilder(
column: $table.proOnly,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get sortOrder => $composableBuilder(
column: $table.sortOrder,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get oneTime => $composableBuilder(
column: $table.oneTime,
builder: (column) => ColumnOrderings(column),
);
} }
class $$WebxdcAppsTableAnnotationComposer class $$WebxdcAppsTableAnnotationComposer
@ -26002,6 +26182,15 @@ class $$WebxdcAppsTableAnnotationComposer
GeneratedColumn<int> get cachedAt => GeneratedColumn<int> get cachedAt =>
$composableBuilder(column: $table.cachedAt, builder: (column) => column); $composableBuilder(column: $table.cachedAt, builder: (column) => column);
GeneratedColumn<bool> get proOnly =>
$composableBuilder(column: $table.proOnly, builder: (column) => column);
GeneratedColumn<int> get sortOrder =>
$composableBuilder(column: $table.sortOrder, builder: (column) => column);
GeneratedColumn<bool> get oneTime =>
$composableBuilder(column: $table.oneTime, builder: (column) => column);
} }
class $$WebxdcAppsTableTableManager class $$WebxdcAppsTableTableManager
@ -26043,6 +26232,9 @@ class $$WebxdcAppsTableTableManager
Value<int> bundleBytes = const Value.absent(), Value<int> bundleBytes = const Value.absent(),
Value<bool> published = const Value.absent(), Value<bool> published = const Value.absent(),
Value<int> cachedAt = const Value.absent(), Value<int> cachedAt = const Value.absent(),
Value<bool> proOnly = const Value.absent(),
Value<int> sortOrder = const Value.absent(),
Value<bool> oneTime = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => WebxdcAppsCompanion( }) => WebxdcAppsCompanion(
appId: appId, appId: appId,
@ -26056,6 +26248,9 @@ class $$WebxdcAppsTableTableManager
bundleBytes: bundleBytes, bundleBytes: bundleBytes,
published: published, published: published,
cachedAt: cachedAt, cachedAt: cachedAt,
proOnly: proOnly,
sortOrder: sortOrder,
oneTime: oneTime,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@ -26071,6 +26266,9 @@ class $$WebxdcAppsTableTableManager
required int bundleBytes, required int bundleBytes,
Value<bool> published = const Value.absent(), Value<bool> published = const Value.absent(),
required int cachedAt, required int cachedAt,
Value<bool> proOnly = const Value.absent(),
Value<int> sortOrder = const Value.absent(),
Value<bool> oneTime = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => WebxdcAppsCompanion.insert( }) => WebxdcAppsCompanion.insert(
appId: appId, appId: appId,
@ -26084,6 +26282,9 @@ class $$WebxdcAppsTableTableManager
bundleBytes: bundleBytes, bundleBytes: bundleBytes,
published: published, published: published,
cachedAt: cachedAt, cachedAt: cachedAt,
proOnly: proOnly,
sortOrder: sortOrder,
oneTime: oneTime,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0

View file

@ -98,6 +98,12 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// No description provided for @subscriptionAllApps.
///
/// In en, this message translates to:
/// **'Access to all apps'**
String get subscriptionAllApps;
/// No description provided for @registerSlogan. /// No description provided for @registerSlogan.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -4736,6 +4742,30 @@ abstract class AppLocalizations {
/// **'The app could not be added to this chat.'** /// **'The app could not be added to this chat.'**
String get webxdcStoreFailed; String get webxdcStoreFailed;
/// No description provided for @webxdcStoreAlreadyAdded.
///
/// In en, this message translates to:
/// **'Already added to this chat'**
String get webxdcStoreAlreadyAdded;
/// No description provided for @webxdcChatAppsMenu.
///
/// In en, this message translates to:
/// **'Chat apps'**
String get webxdcChatAppsMenu;
/// No description provided for @webxdcChatAppsTitle.
///
/// In en, this message translates to:
/// **'Chat apps'**
String get webxdcChatAppsTitle;
/// No description provided for @webxdcChatAppsEmpty.
///
/// In en, this message translates to:
/// **'No one-time apps have been added to this chat yet.'**
String get webxdcChatAppsEmpty;
/// No description provided for @webxdcUnavailable. /// No description provided for @webxdcUnavailable.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -8,6 +8,9 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations { class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get subscriptionAllApps => 'Zugriff auf alle Apps';
@override @override
String get registerSlogan => 'Privat in Kontakt bleiben.'; String get registerSlogan => 'Privat in Kontakt bleiben.';
@ -2754,6 +2757,19 @@ class AppLocalizationsDe extends AppLocalizations {
String get webxdcStoreFailed => String get webxdcStoreFailed =>
'Die App konnte diesem Chat nicht hinzugefügt werden.'; 'Die App konnte diesem Chat nicht hinzugefügt werden.';
@override
String get webxdcStoreAlreadyAdded => 'Bereits zu diesem Chat hinzugefügt';
@override
String get webxdcChatAppsMenu => 'Chat-Apps';
@override
String get webxdcChatAppsTitle => 'Chat-Apps';
@override
String get webxdcChatAppsEmpty =>
'Diesem Chat wurden noch keine einmaligen Apps hinzugefügt.';
@override @override
String get webxdcUnavailable => 'Diese App konnte nicht geladen werden.'; String get webxdcUnavailable => 'Diese App konnte nicht geladen werden.';

View file

@ -8,6 +8,9 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations { class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get subscriptionAllApps => 'Access to all apps';
@override @override
String get registerSlogan => 'Stay in touch privately.'; String get registerSlogan => 'Stay in touch privately.';
@ -2726,6 +2729,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get webxdcStoreFailed => 'The app could not be added to this chat.'; String get webxdcStoreFailed => 'The app could not be added to this chat.';
@override
String get webxdcStoreAlreadyAdded => 'Already added to this chat';
@override
String get webxdcChatAppsMenu => 'Chat apps';
@override
String get webxdcChatAppsTitle => 'Chat apps';
@override
String get webxdcChatAppsEmpty =>
'No one-time apps have been added to this chat yet.';
@override @override
String get webxdcUnavailable => 'This app could not be loaded.'; String get webxdcUnavailable => 'This app could not be loaded.';

View file

@ -1,4 +1,5 @@
{ {
"subscriptionAllApps": "Zugriff auf alle Apps",
"@@locale": "de", "@@locale": "de",
"registerSlogan": "Privat in Kontakt bleiben.", "registerSlogan": "Privat in Kontakt bleiben.",
"onboardingWelcomeTitle": "Willkommen bei twonly!", "onboardingWelcomeTitle": "Willkommen bei twonly!",
@ -1037,6 +1038,10 @@
"webxdcStoreEmpty": "Es sind noch keine Apps verfügbar.", "webxdcStoreEmpty": "Es sind noch keine Apps verfügbar.",
"webxdcStoreOffline": "Die App-Liste konnte nicht geladen werden. Prüfe deine Verbindung und versuche es erneut.", "webxdcStoreOffline": "Die App-Liste konnte nicht geladen werden. Prüfe deine Verbindung und versuche es erneut.",
"webxdcStoreFailed": "Die App konnte diesem Chat nicht hinzugefügt werden.", "webxdcStoreFailed": "Die App konnte diesem Chat nicht hinzugefügt werden.",
"webxdcStoreAlreadyAdded": "Bereits zu diesem Chat hinzugefügt",
"webxdcChatAppsMenu": "Chat-Apps",
"webxdcChatAppsTitle": "Chat-Apps",
"webxdcChatAppsEmpty": "Diesem Chat wurden noch keine einmaligen Apps hinzugefügt.",
"webxdcUnavailable": "Diese App konnte nicht geladen werden.", "webxdcUnavailable": "Diese App konnte nicht geladen werden.",
"webxdcDeleteConfirm": "Alles, was diese App auf diesem Gerät gespeichert hat, wird gelöscht. Andere Mitglieder behalten ihre eigene Kopie.", "webxdcDeleteConfirm": "Alles, was diese App auf diesem Gerät gespeichert hat, wird gelöscht. Andere Mitglieder behalten ihre eigene Kopie.",
"webxdcExternalLinkTitle": "twonly verlassen?", "webxdcExternalLinkTitle": "twonly verlassen?",

View file

@ -1,4 +1,5 @@
{ {
"subscriptionAllApps": "Access to all apps",
"@@locale": "en", "@@locale": "en",
"registerSlogan": "Stay in touch privately.", "registerSlogan": "Stay in touch privately.",
"onboardingWelcomeTitle": "Welcome to twonly!", "onboardingWelcomeTitle": "Welcome to twonly!",
@ -1047,6 +1048,10 @@
"webxdcStoreEmpty": "No apps are available yet.", "webxdcStoreEmpty": "No apps are available yet.",
"webxdcStoreOffline": "The app list could not be loaded. Check your connection and try again.", "webxdcStoreOffline": "The app list could not be loaded. Check your connection and try again.",
"webxdcStoreFailed": "The app could not be added to this chat.", "webxdcStoreFailed": "The app could not be added to this chat.",
"webxdcStoreAlreadyAdded": "Already added to this chat",
"webxdcChatAppsMenu": "Chat apps",
"webxdcChatAppsTitle": "Chat apps",
"webxdcChatAppsEmpty": "No one-time apps have been added to this chat yet.",
"webxdcUnavailable": "This app could not be loaded.", "webxdcUnavailable": "This app could not be loaded.",
"webxdcDeleteConfirm": "Everything this app saved on this device is deleted. Other members keep their own copy.", "webxdcDeleteConfirm": "Everything this app saved on this device is deleted. Other members keep their own copy.",
"webxdcExternalLinkTitle": "Leave twonly?", "webxdcExternalLinkTitle": "Leave twonly?",

View file

@ -109,6 +109,8 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
WebxdcApp? webxdcApp, WebxdcApp? webxdcApp,
WebxdcUpdate? webxdcUpdate, WebxdcUpdate? webxdcUpdate,
WebxdcOrigin? webxdcOrigin, WebxdcOrigin? webxdcOrigin,
WebxdcSyncChunk? webxdcSync,
WebxdcSyncRequest? webxdcSyncRequest,
}) { }) {
final result = create(); final result = create();
if (type != null) result.type = type; if (type != null) result.type = type;
@ -120,6 +122,8 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
if (webxdcApp != null) result.webxdcApp = webxdcApp; if (webxdcApp != null) result.webxdcApp = webxdcApp;
if (webxdcUpdate != null) result.webxdcUpdate = webxdcUpdate; if (webxdcUpdate != null) result.webxdcUpdate = webxdcUpdate;
if (webxdcOrigin != null) result.webxdcOrigin = webxdcOrigin; if (webxdcOrigin != null) result.webxdcOrigin = webxdcOrigin;
if (webxdcSync != null) result.webxdcSync = webxdcSync;
if (webxdcSyncRequest != null) result.webxdcSyncRequest = webxdcSyncRequest;
return result; return result;
} }
@ -148,6 +152,10 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
subBuilder: WebxdcUpdate.create) subBuilder: WebxdcUpdate.create)
..aOM<WebxdcOrigin>(8, _omitFieldNames ? '' : 'webxdcOrigin', ..aOM<WebxdcOrigin>(8, _omitFieldNames ? '' : 'webxdcOrigin',
subBuilder: WebxdcOrigin.create) subBuilder: WebxdcOrigin.create)
..aOM<WebxdcSyncChunk>(9, _omitFieldNames ? '' : 'webxdcSync',
subBuilder: WebxdcSyncChunk.create)
..aOM<WebxdcSyncRequest>(10, _omitFieldNames ? '' : 'webxdcSyncRequest',
subBuilder: WebxdcSyncRequest.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -241,6 +249,464 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
void clearWebxdcOrigin() => $_clearField(8); void clearWebxdcOrigin() => $_clearField(8);
@$pb.TagNumber(8) @$pb.TagNumber(8)
WebxdcOrigin ensureWebxdcOrigin() => $_ensure(7); WebxdcOrigin ensureWebxdcOrigin() => $_ensure(7);
@$pb.TagNumber(9)
WebxdcSyncChunk get webxdcSync => $_getN(8);
@$pb.TagNumber(9)
set webxdcSync(WebxdcSyncChunk value) => $_setField(9, value);
@$pb.TagNumber(9)
$core.bool hasWebxdcSync() => $_has(8);
@$pb.TagNumber(9)
void clearWebxdcSync() => $_clearField(9);
@$pb.TagNumber(9)
WebxdcSyncChunk ensureWebxdcSync() => $_ensure(8);
@$pb.TagNumber(10)
WebxdcSyncRequest get webxdcSyncRequest => $_getN(9);
@$pb.TagNumber(10)
set webxdcSyncRequest(WebxdcSyncRequest value) => $_setField(10, value);
@$pb.TagNumber(10)
$core.bool hasWebxdcSyncRequest() => $_has(9);
@$pb.TagNumber(10)
void clearWebxdcSyncRequest() => $_clearField(10);
@$pb.TagNumber(10)
WebxdcSyncRequest ensureWebxdcSyncRequest() => $_ensure(9);
}
/// A peer-to-peer transfer of existing one-time app state. The encoded payload
/// is split into bounded hidden messages and installed only after every chunk
/// arrived and the digest matches.
class WebxdcSyncChunk extends $pb.GeneratedMessage {
factory WebxdcSyncChunk({
$core.String? transferId,
$core.int? chunkIndex,
$core.int? chunkCount,
$core.List<$core.int>? payloadSha256,
$core.List<$core.int>? payload,
}) {
final result = create();
if (transferId != null) result.transferId = transferId;
if (chunkIndex != null) result.chunkIndex = chunkIndex;
if (chunkCount != null) result.chunkCount = chunkCount;
if (payloadSha256 != null) result.payloadSha256 = payloadSha256;
if (payload != null) result.payload = payload;
return result;
}
WebxdcSyncChunk._();
factory WebxdcSyncChunk.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory WebxdcSyncChunk.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'WebxdcSyncChunk',
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'transferId')
..aI(2, _omitFieldNames ? '' : 'chunkIndex', fieldType: $pb.PbFieldType.OU3)
..aI(3, _omitFieldNames ? '' : 'chunkCount', fieldType: $pb.PbFieldType.OU3)
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'payloadSha256', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'payload', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncChunk clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncChunk copyWith(void Function(WebxdcSyncChunk) updates) =>
super.copyWith((message) => updates(message as WebxdcSyncChunk))
as WebxdcSyncChunk;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static WebxdcSyncChunk create() => WebxdcSyncChunk._();
@$core.override
WebxdcSyncChunk createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static WebxdcSyncChunk getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<WebxdcSyncChunk>(create);
static WebxdcSyncChunk? _defaultInstance;
@$pb.TagNumber(1)
$core.String get transferId => $_getSZ(0);
@$pb.TagNumber(1)
set transferId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasTransferId() => $_has(0);
@$pb.TagNumber(1)
void clearTransferId() => $_clearField(1);
@$pb.TagNumber(2)
$core.int get chunkIndex => $_getIZ(1);
@$pb.TagNumber(2)
set chunkIndex($core.int value) => $_setUnsignedInt32(1, value);
@$pb.TagNumber(2)
$core.bool hasChunkIndex() => $_has(1);
@$pb.TagNumber(2)
void clearChunkIndex() => $_clearField(2);
@$pb.TagNumber(3)
$core.int get chunkCount => $_getIZ(2);
@$pb.TagNumber(3)
set chunkCount($core.int value) => $_setUnsignedInt32(2, value);
@$pb.TagNumber(3)
$core.bool hasChunkCount() => $_has(2);
@$pb.TagNumber(3)
void clearChunkCount() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get payloadSha256 => $_getN(3);
@$pb.TagNumber(4)
set payloadSha256($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasPayloadSha256() => $_has(3);
@$pb.TagNumber(4)
void clearPayloadSha256() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get payload => $_getN(4);
@$pb.TagNumber(5)
set payload($core.List<$core.int> value) => $_setBytes(4, value);
@$pb.TagNumber(5)
$core.bool hasPayload() => $_has(4);
@$pb.TagNumber(5)
void clearPayload() => $_clearField(5);
}
class WebxdcSyncRequest extends $pb.GeneratedMessage {
factory WebxdcSyncRequest() => create();
WebxdcSyncRequest._();
factory WebxdcSyncRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory WebxdcSyncRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'WebxdcSyncRequest',
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncRequest copyWith(void Function(WebxdcSyncRequest) updates) =>
super.copyWith((message) => updates(message as WebxdcSyncRequest))
as WebxdcSyncRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static WebxdcSyncRequest create() => WebxdcSyncRequest._();
@$core.override
WebxdcSyncRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static WebxdcSyncRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<WebxdcSyncRequest>(create);
static WebxdcSyncRequest? _defaultInstance;
}
class WebxdcSyncPayload extends $pb.GeneratedMessage {
factory WebxdcSyncPayload({
$core.Iterable<WebxdcSyncInstance>? instances,
}) {
final result = create();
if (instances != null) result.instances.addAll(instances);
return result;
}
WebxdcSyncPayload._();
factory WebxdcSyncPayload.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory WebxdcSyncPayload.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'WebxdcSyncPayload',
createEmptyInstance: create)
..pPM<WebxdcSyncInstance>(1, _omitFieldNames ? '' : 'instances',
subBuilder: WebxdcSyncInstance.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncPayload clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncPayload copyWith(void Function(WebxdcSyncPayload) updates) =>
super.copyWith((message) => updates(message as WebxdcSyncPayload))
as WebxdcSyncPayload;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static WebxdcSyncPayload create() => WebxdcSyncPayload._();
@$core.override
WebxdcSyncPayload createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static WebxdcSyncPayload getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<WebxdcSyncPayload>(create);
static WebxdcSyncPayload? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<WebxdcSyncInstance> get instances => $_getList(0);
}
class WebxdcSyncInstance extends $pb.GeneratedMessage {
factory WebxdcSyncInstance({
$core.String? instanceId,
$core.String? appId,
$fixnum.Int64? version,
$core.String? summary,
$core.String? document,
$fixnum.Int64? createdAt,
$fixnum.Int64? lastUpdateAt,
$core.Iterable<WebxdcSyncUpdate>? updates,
}) {
final result = create();
if (instanceId != null) result.instanceId = instanceId;
if (appId != null) result.appId = appId;
if (version != null) result.version = version;
if (summary != null) result.summary = summary;
if (document != null) result.document = document;
if (createdAt != null) result.createdAt = createdAt;
if (lastUpdateAt != null) result.lastUpdateAt = lastUpdateAt;
if (updates != null) result.updates.addAll(updates);
return result;
}
WebxdcSyncInstance._();
factory WebxdcSyncInstance.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory WebxdcSyncInstance.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'WebxdcSyncInstance',
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'instanceId')
..aOS(2, _omitFieldNames ? '' : 'appId')
..aInt64(3, _omitFieldNames ? '' : 'version')
..aOS(4, _omitFieldNames ? '' : 'summary')
..aOS(5, _omitFieldNames ? '' : 'document')
..aInt64(6, _omitFieldNames ? '' : 'createdAt')
..aInt64(7, _omitFieldNames ? '' : 'lastUpdateAt')
..pPM<WebxdcSyncUpdate>(8, _omitFieldNames ? '' : 'updates',
subBuilder: WebxdcSyncUpdate.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncInstance clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncInstance copyWith(void Function(WebxdcSyncInstance) updates) =>
super.copyWith((message) => updates(message as WebxdcSyncInstance))
as WebxdcSyncInstance;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static WebxdcSyncInstance create() => WebxdcSyncInstance._();
@$core.override
WebxdcSyncInstance createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static WebxdcSyncInstance getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<WebxdcSyncInstance>(create);
static WebxdcSyncInstance? _defaultInstance;
@$pb.TagNumber(1)
$core.String get instanceId => $_getSZ(0);
@$pb.TagNumber(1)
set instanceId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasInstanceId() => $_has(0);
@$pb.TagNumber(1)
void clearInstanceId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get appId => $_getSZ(1);
@$pb.TagNumber(2)
set appId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasAppId() => $_has(1);
@$pb.TagNumber(2)
void clearAppId() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get version => $_getI64(2);
@$pb.TagNumber(3)
set version($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasVersion() => $_has(2);
@$pb.TagNumber(3)
void clearVersion() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get summary => $_getSZ(3);
@$pb.TagNumber(4)
set summary($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasSummary() => $_has(3);
@$pb.TagNumber(4)
void clearSummary() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get document => $_getSZ(4);
@$pb.TagNumber(5)
set document($core.String value) => $_setString(4, value);
@$pb.TagNumber(5)
$core.bool hasDocument() => $_has(4);
@$pb.TagNumber(5)
void clearDocument() => $_clearField(5);
@$pb.TagNumber(6)
$fixnum.Int64 get createdAt => $_getI64(5);
@$pb.TagNumber(6)
set createdAt($fixnum.Int64 value) => $_setInt64(5, value);
@$pb.TagNumber(6)
$core.bool hasCreatedAt() => $_has(5);
@$pb.TagNumber(6)
void clearCreatedAt() => $_clearField(6);
@$pb.TagNumber(7)
$fixnum.Int64 get lastUpdateAt => $_getI64(6);
@$pb.TagNumber(7)
set lastUpdateAt($fixnum.Int64 value) => $_setInt64(6, value);
@$pb.TagNumber(7)
$core.bool hasLastUpdateAt() => $_has(6);
@$pb.TagNumber(7)
void clearLastUpdateAt() => $_clearField(7);
@$pb.TagNumber(8)
$pb.PbList<WebxdcSyncUpdate> get updates => $_getList(7);
}
class WebxdcSyncUpdate extends $pb.GeneratedMessage {
factory WebxdcSyncUpdate({
$core.String? messageId,
$fixnum.Int64? senderId,
$core.String? payload,
$core.String? info,
$core.String? href,
$fixnum.Int64? receivedAt,
}) {
final result = create();
if (messageId != null) result.messageId = messageId;
if (senderId != null) result.senderId = senderId;
if (payload != null) result.payload = payload;
if (info != null) result.info = info;
if (href != null) result.href = href;
if (receivedAt != null) result.receivedAt = receivedAt;
return result;
}
WebxdcSyncUpdate._();
factory WebxdcSyncUpdate.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory WebxdcSyncUpdate.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'WebxdcSyncUpdate',
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'messageId')
..aInt64(2, _omitFieldNames ? '' : 'senderId')
..aOS(3, _omitFieldNames ? '' : 'payload')
..aOS(4, _omitFieldNames ? '' : 'info')
..aOS(5, _omitFieldNames ? '' : 'href')
..aInt64(6, _omitFieldNames ? '' : 'receivedAt')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncUpdate clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
WebxdcSyncUpdate copyWith(void Function(WebxdcSyncUpdate) updates) =>
super.copyWith((message) => updates(message as WebxdcSyncUpdate))
as WebxdcSyncUpdate;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static WebxdcSyncUpdate create() => WebxdcSyncUpdate._();
@$core.override
WebxdcSyncUpdate createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static WebxdcSyncUpdate getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<WebxdcSyncUpdate>(create);
static WebxdcSyncUpdate? _defaultInstance;
@$pb.TagNumber(1)
$core.String get messageId => $_getSZ(0);
@$pb.TagNumber(1)
set messageId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasMessageId() => $_has(0);
@$pb.TagNumber(1)
void clearMessageId() => $_clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get senderId => $_getI64(1);
@$pb.TagNumber(2)
set senderId($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasSenderId() => $_has(1);
@$pb.TagNumber(2)
void clearSenderId() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get payload => $_getSZ(2);
@$pb.TagNumber(3)
set payload($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasPayload() => $_has(2);
@$pb.TagNumber(3)
void clearPayload() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get info => $_getSZ(3);
@$pb.TagNumber(4)
set info($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasInfo() => $_has(3);
@$pb.TagNumber(4)
void clearInfo() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get href => $_getSZ(4);
@$pb.TagNumber(5)
set href($core.String value) => $_setString(4, value);
@$pb.TagNumber(5)
$core.bool hasHref() => $_has(4);
@$pb.TagNumber(5)
void clearHref() => $_clearField(5);
@$pb.TagNumber(6)
$fixnum.Int64 get receivedAt => $_getI64(5);
@$pb.TagNumber(6)
set receivedAt($fixnum.Int64 value) => $_setInt64(5, value);
@$pb.TagNumber(6)
$core.bool hasReceivedAt() => $_has(5);
@$pb.TagNumber(6)
void clearReceivedAt() => $_clearField(6);
} }
/// Attached to a message a webxdc app asked the user to send, so the chat can /// Attached to a message a webxdc app asked the user to send, so the chat can
@ -402,6 +868,7 @@ class WebxdcUpdate extends $pb.GeneratedMessage {
$core.String? href, $core.String? href,
$core.String? summary, $core.String? summary,
$core.String? document, $core.String? document,
$core.String? notify,
}) { }) {
final result = create(); final result = create();
if (instanceId != null) result.instanceId = instanceId; if (instanceId != null) result.instanceId = instanceId;
@ -410,6 +877,7 @@ class WebxdcUpdate extends $pb.GeneratedMessage {
if (href != null) result.href = href; if (href != null) result.href = href;
if (summary != null) result.summary = summary; if (summary != null) result.summary = summary;
if (document != null) result.document = document; if (document != null) result.document = document;
if (notify != null) result.notify = notify;
return result; return result;
} }
@ -431,6 +899,7 @@ class WebxdcUpdate extends $pb.GeneratedMessage {
..aOS(4, _omitFieldNames ? '' : 'href') ..aOS(4, _omitFieldNames ? '' : 'href')
..aOS(5, _omitFieldNames ? '' : 'summary') ..aOS(5, _omitFieldNames ? '' : 'summary')
..aOS(6, _omitFieldNames ? '' : 'document') ..aOS(6, _omitFieldNames ? '' : 'document')
..aOS(7, _omitFieldNames ? '' : 'notify')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -507,6 +976,17 @@ class WebxdcUpdate extends $pb.GeneratedMessage {
$core.bool hasDocument() => $_has(5); $core.bool hasDocument() => $_has(5);
@$pb.TagNumber(6) @$pb.TagNumber(6)
void clearDocument() => $_clearField(6); void clearDocument() => $_clearField(6);
/// JSON map of app-scoped member addresses to notification text.
/// Absent preserves legacy broadcast; an empty map notifies nobody.
@$pb.TagNumber(7)
$core.String get notify => $_getSZ(6);
@$pb.TagNumber(7)
set notify($core.String value) => $_setString(6, value);
@$pb.TagNumber(7)
$core.bool hasNotify() => $_has(6);
@$pb.TagNumber(7)
void clearNotify() => $_clearField(7);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -30,6 +30,11 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
AdditionalMessageData_Type._(5, _omitEnumNames ? '' : 'WEBXDC_UPDATE'); AdditionalMessageData_Type._(5, _omitEnumNames ? '' : 'WEBXDC_UPDATE');
static const AdditionalMessageData_Type WEBXDC_SENT = static const AdditionalMessageData_Type WEBXDC_SENT =
AdditionalMessageData_Type._(6, _omitEnumNames ? '' : 'WEBXDC_SENT'); AdditionalMessageData_Type._(6, _omitEnumNames ? '' : 'WEBXDC_SENT');
static const AdditionalMessageData_Type WEBXDC_SYNC =
AdditionalMessageData_Type._(7, _omitEnumNames ? '' : 'WEBXDC_SYNC');
static const AdditionalMessageData_Type WEBXDC_SYNC_REQUEST =
AdditionalMessageData_Type._(
8, _omitEnumNames ? '' : 'WEBXDC_SYNC_REQUEST');
static const $core.List<AdditionalMessageData_Type> values = static const $core.List<AdditionalMessageData_Type> values =
<AdditionalMessageData_Type>[ <AdditionalMessageData_Type>[
@ -40,10 +45,12 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
WEBXDC_APP, WEBXDC_APP,
WEBXDC_UPDATE, WEBXDC_UPDATE,
WEBXDC_SENT, WEBXDC_SENT,
WEBXDC_SYNC,
WEBXDC_SYNC_REQUEST,
]; ];
static final $core.List<AdditionalMessageData_Type?> _byValue = static final $core.List<AdditionalMessageData_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 6); $pb.ProtobufEnum.$_initByValueList(values, 8);
static AdditionalMessageData_Type? valueOf($core.int value) => static AdditionalMessageData_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -106,6 +106,26 @@ const AdditionalMessageData$json = {
'10': 'webxdcOrigin', '10': 'webxdcOrigin',
'17': true '17': true
}, },
{
'1': 'webxdc_sync',
'3': 9,
'4': 1,
'5': 11,
'6': '.WebxdcSyncChunk',
'9': 6,
'10': 'webxdcSync',
'17': true
},
{
'1': 'webxdc_sync_request',
'3': 10,
'4': 1,
'5': 11,
'6': '.WebxdcSyncRequest',
'9': 7,
'10': 'webxdcSyncRequest',
'17': true
},
], ],
'4': [AdditionalMessageData_Type$json], '4': [AdditionalMessageData_Type$json],
'8': [ '8': [
@ -115,6 +135,8 @@ const AdditionalMessageData$json = {
{'1': '_webxdc_app'}, {'1': '_webxdc_app'},
{'1': '_webxdc_update'}, {'1': '_webxdc_update'},
{'1': '_webxdc_origin'}, {'1': '_webxdc_origin'},
{'1': '_webxdc_sync'},
{'1': '_webxdc_sync_request'},
], ],
}; };
@ -129,6 +151,8 @@ const AdditionalMessageData_Type$json = {
{'1': 'WEBXDC_APP', '2': 4}, {'1': 'WEBXDC_APP', '2': 4},
{'1': 'WEBXDC_UPDATE', '2': 5}, {'1': 'WEBXDC_UPDATE', '2': 5},
{'1': 'WEBXDC_SENT', '2': 6}, {'1': 'WEBXDC_SENT', '2': 6},
{'1': 'WEBXDC_SYNC', '2': 7},
{'1': 'WEBXDC_SYNC_REQUEST', '2': 8},
], ],
}; };
@ -141,11 +165,148 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec
'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQESLgoKd2VieGRjX2FwcBgGIAEoCzIKLldlYnhkY0' 'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQESLgoKd2VieGRjX2FwcBgGIAEoCzIKLldlYnhkY0'
'FwcEgDUgl3ZWJ4ZGNBcHCIAQESNwoNd2VieGRjX3VwZGF0ZRgHIAEoCzINLldlYnhkY1VwZGF0' 'FwcEgDUgl3ZWJ4ZGNBcHCIAQESNwoNd2VieGRjX3VwZGF0ZRgHIAEoCzINLldlYnhkY1VwZGF0'
'ZUgEUgx3ZWJ4ZGNVcGRhdGWIAQESNwoNd2VieGRjX29yaWdpbhgIIAEoCzINLldlYnhkY09yaW' 'ZUgEUgx3ZWJ4ZGNVcGRhdGWIAQESNwoNd2VieGRjX29yaWdpbhgIIAEoCzINLldlYnhkY09yaW'
'dpbkgFUgx3ZWJ4ZGNPcmlnaW6IAQEiggEKBFR5cGUSCAoETElOSxAAEgwKCENPTlRBQ1RTEAES' 'dpbkgFUgx3ZWJ4ZGNPcmlnaW6IAQESNgoLd2VieGRjX3N5bmMYCSABKAsyEC5XZWJ4ZGNTeW5j'
'GgoWUkVTVE9SRURfRkxBTUVfQ09VTlRFUhACEhIKDkFTS19BQk9VVF9VU0VSEAMSDgoKV0VCWE' 'Q2h1bmtIBlIKd2VieGRjU3luY4gBARJHChN3ZWJ4ZGNfc3luY19yZXF1ZXN0GAogASgLMhIuV2'
'RDX0FQUBAEEhEKDVdFQlhEQ19VUERBVEUQBRIPCgtXRUJYRENfU0VOVBAGQgcKBV9saW5rQhkK' 'VieGRjU3luY1JlcXVlc3RIB1IRd2VieGRjU3luY1JlcXVlc3SIAQEirAEKBFR5cGUSCAoETElO'
'F19yZXN0b3JlZF9mbGFtZV9jb3VudGVyQhQKEl9hc2tfYWJvdXRfdXNlcl9pZEINCgtfd2VieG' 'SxAAEgwKCENPTlRBQ1RTEAESGgoWUkVTVE9SRURfRkxBTUVfQ09VTlRFUhACEhIKDkFTS19BQk'
'RjX2FwcEIQCg5fd2VieGRjX3VwZGF0ZUIQCg5fd2VieGRjX29yaWdpbg=='); '9VVF9VU0VSEAMSDgoKV0VCWERDX0FQUBAEEhEKDVdFQlhEQ19VUERBVEUQBRIPCgtXRUJYRENf'
'U0VOVBAGEg8KC1dFQlhEQ19TWU5DEAcSFwoTV0VCWERDX1NZTkNfUkVRVUVTVBAIQgcKBV9saW'
'5rQhkKF19yZXN0b3JlZF9mbGFtZV9jb3VudGVyQhQKEl9hc2tfYWJvdXRfdXNlcl9pZEINCgtf'
'd2VieGRjX2FwcEIQCg5fd2VieGRjX3VwZGF0ZUIQCg5fd2VieGRjX29yaWdpbkIOCgxfd2VieG'
'RjX3N5bmNCFgoUX3dlYnhkY19zeW5jX3JlcXVlc3Q=');
@$core.Deprecated('Use webxdcSyncChunkDescriptor instead')
const WebxdcSyncChunk$json = {
'1': 'WebxdcSyncChunk',
'2': [
{'1': 'transfer_id', '3': 1, '4': 1, '5': 9, '10': 'transferId'},
{'1': 'chunk_index', '3': 2, '4': 1, '5': 13, '10': 'chunkIndex'},
{'1': 'chunk_count', '3': 3, '4': 1, '5': 13, '10': 'chunkCount'},
{'1': 'payload_sha256', '3': 4, '4': 1, '5': 12, '10': 'payloadSha256'},
{'1': 'payload', '3': 5, '4': 1, '5': 12, '10': 'payload'},
],
};
/// Descriptor for `WebxdcSyncChunk`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List webxdcSyncChunkDescriptor = $convert.base64Decode(
'Cg9XZWJ4ZGNTeW5jQ2h1bmsSHwoLdHJhbnNmZXJfaWQYASABKAlSCnRyYW5zZmVySWQSHwoLY2'
'h1bmtfaW5kZXgYAiABKA1SCmNodW5rSW5kZXgSHwoLY2h1bmtfY291bnQYAyABKA1SCmNodW5r'
'Q291bnQSJQoOcGF5bG9hZF9zaGEyNTYYBCABKAxSDXBheWxvYWRTaGEyNTYSGAoHcGF5bG9hZB'
'gFIAEoDFIHcGF5bG9hZA==');
@$core.Deprecated('Use webxdcSyncRequestDescriptor instead')
const WebxdcSyncRequest$json = {
'1': 'WebxdcSyncRequest',
};
/// Descriptor for `WebxdcSyncRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List webxdcSyncRequestDescriptor =
$convert.base64Decode('ChFXZWJ4ZGNTeW5jUmVxdWVzdA==');
@$core.Deprecated('Use webxdcSyncPayloadDescriptor instead')
const WebxdcSyncPayload$json = {
'1': 'WebxdcSyncPayload',
'2': [
{
'1': 'instances',
'3': 1,
'4': 3,
'5': 11,
'6': '.WebxdcSyncInstance',
'10': 'instances'
},
],
};
/// Descriptor for `WebxdcSyncPayload`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List webxdcSyncPayloadDescriptor = $convert.base64Decode(
'ChFXZWJ4ZGNTeW5jUGF5bG9hZBIxCglpbnN0YW5jZXMYASADKAsyEy5XZWJ4ZGNTeW5jSW5zdG'
'FuY2VSCWluc3RhbmNlcw==');
@$core.Deprecated('Use webxdcSyncInstanceDescriptor instead')
const WebxdcSyncInstance$json = {
'1': 'WebxdcSyncInstance',
'2': [
{'1': 'instance_id', '3': 1, '4': 1, '5': 9, '10': 'instanceId'},
{'1': 'app_id', '3': 2, '4': 1, '5': 9, '10': 'appId'},
{'1': 'version', '3': 3, '4': 1, '5': 3, '10': 'version'},
{
'1': 'summary',
'3': 4,
'4': 1,
'5': 9,
'9': 0,
'10': 'summary',
'17': true
},
{
'1': 'document',
'3': 5,
'4': 1,
'5': 9,
'9': 1,
'10': 'document',
'17': true
},
{'1': 'created_at', '3': 6, '4': 1, '5': 3, '10': 'createdAt'},
{'1': 'last_update_at', '3': 7, '4': 1, '5': 3, '10': 'lastUpdateAt'},
{
'1': 'updates',
'3': 8,
'4': 3,
'5': 11,
'6': '.WebxdcSyncUpdate',
'10': 'updates'
},
],
'8': [
{'1': '_summary'},
{'1': '_document'},
],
};
/// Descriptor for `WebxdcSyncInstance`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List webxdcSyncInstanceDescriptor = $convert.base64Decode(
'ChJXZWJ4ZGNTeW5jSW5zdGFuY2USHwoLaW5zdGFuY2VfaWQYASABKAlSCmluc3RhbmNlSWQSFQ'
'oGYXBwX2lkGAIgASgJUgVhcHBJZBIYCgd2ZXJzaW9uGAMgASgDUgd2ZXJzaW9uEh0KB3N1bW1h'
'cnkYBCABKAlIAFIHc3VtbWFyeYgBARIfCghkb2N1bWVudBgFIAEoCUgBUghkb2N1bWVudIgBAR'
'IdCgpjcmVhdGVkX2F0GAYgASgDUgljcmVhdGVkQXQSJAoObGFzdF91cGRhdGVfYXQYByABKANS'
'DGxhc3RVcGRhdGVBdBIrCgd1cGRhdGVzGAggAygLMhEuV2VieGRjU3luY1VwZGF0ZVIHdXBkYX'
'Rlc0IKCghfc3VtbWFyeUILCglfZG9jdW1lbnQ=');
@$core.Deprecated('Use webxdcSyncUpdateDescriptor instead')
const WebxdcSyncUpdate$json = {
'1': 'WebxdcSyncUpdate',
'2': [
{'1': 'message_id', '3': 1, '4': 1, '5': 9, '10': 'messageId'},
{
'1': 'sender_id',
'3': 2,
'4': 1,
'5': 3,
'9': 0,
'10': 'senderId',
'17': true
},
{'1': 'payload', '3': 3, '4': 1, '5': 9, '10': 'payload'},
{'1': 'info', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'info', '17': true},
{'1': 'href', '3': 5, '4': 1, '5': 9, '9': 2, '10': 'href', '17': true},
{'1': 'received_at', '3': 6, '4': 1, '5': 3, '10': 'receivedAt'},
],
'8': [
{'1': '_sender_id'},
{'1': '_info'},
{'1': '_href'},
],
};
/// Descriptor for `WebxdcSyncUpdate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List webxdcSyncUpdateDescriptor = $convert.base64Decode(
'ChBXZWJ4ZGNTeW5jVXBkYXRlEh0KCm1lc3NhZ2VfaWQYASABKAlSCW1lc3NhZ2VJZBIgCglzZW'
'5kZXJfaWQYAiABKANIAFIIc2VuZGVySWSIAQESGAoHcGF5bG9hZBgDIAEoCVIHcGF5bG9hZBIX'
'CgRpbmZvGAQgASgJSAFSBGluZm+IAQESFwoEaHJlZhgFIAEoCUgCUgRocmVmiAEBEh8KC3JlY2'
'VpdmVkX2F0GAYgASgDUgpyZWNlaXZlZEF0QgwKCl9zZW5kZXJfaWRCBwoFX2luZm9CBwoFX2hy'
'ZWY=');
@$core.Deprecated('Use webxdcOriginDescriptor instead') @$core.Deprecated('Use webxdcOriginDescriptor instead')
const WebxdcOrigin$json = { const WebxdcOrigin$json = {
@ -202,12 +363,14 @@ const WebxdcUpdate$json = {
'10': 'document', '10': 'document',
'17': true '17': true
}, },
{'1': 'notify', '3': 7, '4': 1, '5': 9, '9': 4, '10': 'notify', '17': true},
], ],
'8': [ '8': [
{'1': '_info'}, {'1': '_info'},
{'1': '_href'}, {'1': '_href'},
{'1': '_summary'}, {'1': '_summary'},
{'1': '_document'}, {'1': '_document'},
{'1': '_notify'},
], ],
}; };
@ -216,5 +379,5 @@ final $typed_data.Uint8List webxdcUpdateDescriptor = $convert.base64Decode(
'CgxXZWJ4ZGNVcGRhdGUSHwoLaW5zdGFuY2VfaWQYASABKAlSCmluc3RhbmNlSWQSGAoHcGF5bG' 'CgxXZWJ4ZGNVcGRhdGUSHwoLaW5zdGFuY2VfaWQYASABKAlSCmluc3RhbmNlSWQSGAoHcGF5bG'
'9hZBgCIAEoCVIHcGF5bG9hZBIXCgRpbmZvGAMgASgJSABSBGluZm+IAQESFwoEaHJlZhgEIAEo' '9hZBgCIAEoCVIHcGF5bG9hZBIXCgRpbmZvGAMgASgJSABSBGluZm+IAQESFwoEaHJlZhgEIAEo'
'CUgBUgRocmVmiAEBEh0KB3N1bW1hcnkYBSABKAlIAlIHc3VtbWFyeYgBARIfCghkb2N1bWVudB' 'CUgBUgRocmVmiAEBEh0KB3N1bW1hcnkYBSABKAlIAlIHc3VtbWFyeYgBARIfCghkb2N1bWVudB'
'gGIAEoCUgDUghkb2N1bWVudIgBAUIHCgVfaW5mb0IHCgVfaHJlZkIKCghfc3VtbWFyeUILCglf' 'gGIAEoCUgDUghkb2N1bWVudIgBARIbCgZub3RpZnkYByABKAlIBFIGbm90aWZ5iAEBQgcKBV9p'
'ZG9jdW1lbnQ='); 'bmZvQgcKBV9ocmVmQgoKCF9zdW1tYXJ5QgsKCV9kb2N1bWVudEIJCgdfbm90aWZ5');

View file

@ -46,16 +46,32 @@ class WebxdcService {
/// cached with all its translations, so choosing one costs nothing and is /// cached with all its translations, so choosing one costs nothing and is
/// never sent anywhere. /// never sent anywhere.
static Future<List<rust_webxdc.WebxdcStoreApp>> catalog( static Future<List<rust_webxdc.WebxdcStoreApp>> catalog(
String groupId,
List<String> languages, List<String> languages,
) async { ) async {
try { try {
return await rust_webxdc.catalog(languages: languages); return await rust_webxdc.catalog(groupId: groupId, languages: languages);
} catch (error) { } catch (error) {
Log.warn('reading the webxdc catalog failed: $error'); Log.warn('reading the webxdc catalog failed: $error');
return []; return [];
} }
} }
static Future<List<rust_webxdc.WebxdcOneTimeInstance>> oneTimeInstances(
String groupId,
List<String> languages,
) async {
try {
return await rust_webxdc.oneTimeInstances(
groupId: groupId,
languages: languages,
);
} catch (error) {
Log.warn('reading one-time webxdc apps for $groupId failed: $error');
return [];
}
}
/// Places an app into a chat. Returns the id of the message carrying it, /// Places an app into a chat. Returns the id of the message carrying it,
/// which is also the instance id. /// which is also the instance id.
static Future<String?> createInstance( static Future<String?> createInstance(
@ -77,10 +93,8 @@ class WebxdcService {
/// Gets the bundle an instance runs onto disk, verified. /// Gets the bundle an instance runs onto disk, verified.
/// ///
/// Rust checks the store for a newer version first and moves the instance to /// Opens cached code without a network update check. Only a bundle that
/// it if there is one, so an app updates between two runs and never during /// has not been downloaded yet needs the server.
/// one. Called when the user starts an app and never on message arrival: a
/// message must not be able to make a device fetch anything.
static Future<String?> prepareBundle(String instanceId) async { static Future<String?> prepareBundle(String instanceId) async {
try { try {
return await rust_webxdc.prepareBundle(instanceId: instanceId); return await rust_webxdc.prepareBundle(instanceId: instanceId);
@ -90,6 +104,20 @@ class WebxdcService {
} }
} }
static final Set<String> _updatingInstances = {};
/// Best effort after closing. A failed check leaves the existing code usable.
static Future<void> cacheUpdate(String instanceId) async {
if (!_updatingInstances.add(instanceId)) return;
try {
await rust_webxdc.cacheUpdate(instanceId: instanceId);
} catch (error) {
Log.warn('checking for a webxdc update after closing failed: $error');
} finally {
_updatingInstances.remove(instanceId);
}
}
static Future<rust_webxdc.WebxdcInstanceInfo?> instance( static Future<rust_webxdc.WebxdcInstanceInfo?> instance(
String instanceId, String instanceId,
) async { ) async {
@ -126,6 +154,7 @@ class WebxdcService {
String? href, String? href,
String? summary, String? summary,
String? document, String? document,
String? notify,
}) => rust_webxdc.sendUpdate( }) => rust_webxdc.sendUpdate(
instanceId: instanceId, instanceId: instanceId,
payload: payload, payload: payload,
@ -133,6 +162,7 @@ class WebxdcService {
href: href, href: href,
summary: summary, summary: summary,
document: document, document: document,
notify: notify,
); );
/// Removes an instance, its whole update log, and the web storage its origin /// Removes an instance, its whole update log, and the web storage its origin

View file

@ -90,8 +90,7 @@ class WebxdcHost {
/// Gets an app ready to run: makes sure the bundle is on disk and verified, /// Gets an app ready to run: makes sure the bundle is on disk and verified,
/// and returns what the view needs to show it. /// and returns what the view needs to show it.
/// ///
/// Rust checks the store for a newer version as part of this, so an app /// Cached apps open without a network update check. Returns a failure when the bundle
/// updates here and nowhere else. Returns a failure instead when the bundle
/// cannot be had at all -- a first start with no network, or an app that was /// cannot be had at all -- a first start with no network, or an app that was
/// taken out of the store before this device ever downloaded it. /// taken out of the store before this device ever downloaded it.
/// `languages` is what the reader prefers, most preferred first: the title /// `languages` is what the reader prefers, most preferred first: the title
@ -104,8 +103,7 @@ class WebxdcHost {
if (instance == null) { if (instance == null) {
return const WebxdcLaunch.failed('unavailable'); return const WebxdcLaunch.failed('unavailable');
} }
// The only place a bundle is ever fetched. A message arriving in a chat // A first launch may need a download. Existing bundles are read locally.
// never reaches this.
final bundlePath = await WebxdcService.prepareBundle(instanceId); final bundlePath = await WebxdcService.prepareBundle(instanceId);
if (bundlePath == null) { if (bundlePath == null) {
return const WebxdcLaunch.failed('unavailable'); return const WebxdcLaunch.failed('unavailable');
@ -148,6 +146,8 @@ class WebxdcHost {
_updateSubscription = null; _updateSubscription = null;
_openInstanceId = null; _openInstanceId = null;
_closeView = null; _closeView = null;
// Closing never waits for the server; downloaded code is used next time.
unawaited(WebxdcService.cacheUpdate(instanceId));
} }
static Future<void> _clearOrigin(String origin) async { static Future<void> _clearOrigin(String origin) async {
@ -222,6 +222,10 @@ class WebxdcHost {
final params = (message['params'] as Map<String, dynamic>?) ?? {}; final params = (message['params'] as Map<String, dynamic>?) ?? {};
try { try {
switch (message['method']) { switch (message['method']) {
case 'getMembers':
final members = await rust_webxdc.members(instanceId: instanceId);
return jsonEncode({'id': id, 'result': jsonDecode(members)});
case 'sendUpdate': case 'sendUpdate':
// `payload` is re-encoded rather than passed through, so what is // `payload` is re-encoded rather than passed through, so what is
// stored and sent is JSON this side produced. // stored and sent is JSON this side produced.
@ -232,6 +236,9 @@ class WebxdcHost {
href: params['href'] as String?, href: params['href'] as String?,
summary: params['summary'] as String?, summary: params['summary'] as String?,
document: params['document'] as String?, document: params['document'] as String?,
notify: params['notify'] == null
? null
: jsonEncode(params['notify']),
); );
return jsonEncode({'id': id, 'result': null}); return jsonEncode({'id': id, 'result': null});

View file

@ -45,11 +45,10 @@ class GroupContextMenu extends StatelessWidget {
), ),
ContextMenuItem( ContextMenuItem(
title: context.lang.contextMenuOpenChat, title: context.lang.contextMenuOpenChat,
onTap: () => onTap: () => navigator.context.push(
navigator.context.push( Routes.chatsMessages(group.groupId),
Routes.chatsMessages(group.groupId), extra: group,
extra: group, ),
),
icon: FontAwesomeIcons.comments, icon: FontAwesomeIcons.comments,
), ),
if (!group.archived) if (!group.archived)
@ -76,17 +75,6 @@ class GroupContextMenu extends StatelessWidget {
); );
if (ok) { if (ok) {
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
if (group.isDirectChat) {
await twonlyDB.groupsDao.deleteGroup(group.groupId);
} else {
await twonlyDB.groupsDao.deleteGroup(group.groupId);
// await twonlyDB.groupsDao.updateGroup(
// group.groupId,
// const GroupsCompanion(
// deletedContent: Value(true),
// ),
// );
}
} }
}, },
), ),

View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/utils/misc.dart';
class ProBadge extends StatelessWidget {
const ProBadge({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.amber.shade700,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FaIcon(
FontAwesomeIcons.star,
size: 10,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
context.lang.backupCloudProBadge,
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}

View file

@ -2,11 +2,17 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:twonly/core/bridge/webxdc.dart' as rust_webxdc; import 'package:twonly/core/bridge/webxdc.dart' as rust_webxdc;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/webxdc/webxdc.service.dart'; import 'package:twonly/src/services/webxdc/webxdc.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/pro_badge.element.dart';
/// The in-app store. /// The in-app store.
/// ///
@ -44,11 +50,13 @@ class _WebxdcStoreViewState extends State<WebxdcStoreView> {
// The cached listing is shown first so the sheet is never empty while the // The cached listing is shown first so the sheet is never empty while the
// refresh is in flight. // refresh is in flight.
final languages = [language]; final languages = [language];
final cached = await WebxdcService.catalog(languages); final cached = await WebxdcService.catalog(widget.group.groupId, languages);
if (mounted) setState(() => _apps = cached); if (mounted) setState(() => _apps = cached);
final refreshed = await WebxdcService.refreshCatalog(); final refreshed = await WebxdcService.refreshCatalog();
final apps = refreshed ? await WebxdcService.catalog(languages) : cached; final apps = refreshed
? await WebxdcService.catalog(widget.group.groupId, languages)
: cached;
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_apps = apps; _apps = apps;
@ -57,6 +65,10 @@ class _WebxdcStoreViewState extends State<WebxdcStoreView> {
} }
Future<void> _place(rust_webxdc.WebxdcStoreApp app) async { Future<void> _place(rust_webxdc.WebxdcStoreApp app) async {
if (app.proOnly && !isPayingUser(context.read<PurchasesProvider>().plan)) {
await context.push(Routes.settingsSubscription);
return;
}
setState(() => _placing = app.appId); setState(() => _placing = app.appId);
final instanceId = await WebxdcService.createInstance( final instanceId = await WebxdcService.createInstance(
widget.group.groupId, widget.group.groupId,
@ -140,44 +152,70 @@ class _WebxdcStoreViewState extends State<WebxdcStoreView> {
); );
} }
return ListView.builder( final available = apps
controller: controller, .where((app) => !app.oneTime || app.instanceId == null)
itemCount: apps.length, .toList();
itemBuilder: (context, index) { final alreadyAdded = apps
final app = apps[index]; .where((app) => app.oneTime && app.instanceId != null)
final icon = app.icon; .toList();
// What the app says about itself, in the reader's language, as the
// store published it. Plain text, bounded and stripped of control
// characters before it was ever accepted for publishing.
final subtitle = userService.currentUser.isDeveloper
? ['v${app.version}', ?app.description].join(' · ')
: app.description;
return ListTile( return ListView(
leading: SizedBox( controller: controller,
width: 40, children: [
height: 40, ...available.map(_appTile),
child: icon == null if (alreadyAdded.isNotEmpty) ...[
? const FaIcon(FontAwesomeIcons.puzzlePiece) Padding(
: ClipRRect( padding: const EdgeInsets.fromLTRB(20, 18, 20, 6),
borderRadius: BorderRadius.circular(8), child: Text(
child: Image.memory(icon, fit: BoxFit.cover), context.lang.webxdcStoreAlreadyAdded,
), style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: context.color.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
), ),
title: Text(app.name), ...alreadyAdded.map(
subtitle: subtitle == null (app) =>
? null Opacity(opacity: 0.45, child: _appTile(app, disabled: true)),
: Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis), ),
trailing: _placing == app.appId ],
? const SizedBox( ],
width: 20, );
height: 20, }
child: CircularProgressIndicator(strokeWidth: 2),
) Widget _appTile(
: null, rust_webxdc.WebxdcStoreApp app, {
onTap: _placing == null ? () => _place(app) : null, bool disabled = false,
); }) {
}, final icon = app.icon;
final subtitle = userService.currentUser.isDeveloper
? ['v${app.version}', ?app.description].join(' · ')
: app.description;
return ListTile(
leading: SizedBox(
width: 40,
height: 40,
child: icon == null
? const FaIcon(FontAwesomeIcons.puzzlePiece)
: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(icon, fit: BoxFit.cover),
),
),
title: Text(app.name),
subtitle: subtitle == null
? null
: Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: _placing == app.appId
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: app.proOnly
? const ProBadge()
: null,
onTap: !disabled && _placing == null ? () => _place(app) : null,
); );
} }
} }

View file

@ -24,6 +24,7 @@ import 'package:twonly/src/visual/views/contact/contact_components/user_discover
import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart';
import 'package:twonly/src/visual/views/contact/select_contact_groups.view.dart'; import 'package:twonly/src/visual/views/contact/select_contact_groups.view.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
import 'package:twonly/src/visual/views/webxdc/webxdc_chat_apps.view.dart';
class ContactView extends StatefulWidget { class ContactView extends StatefulWidget {
const ContactView(this.userId, {super.key}); const ContactView(this.userId, {super.key});
@ -233,6 +234,18 @@ class _ContactViewState extends State<ContactView> {
userService.currentUser.userId, userService.currentUser.userId,
), ),
), ),
BetterListTile(
icon: FontAwesomeIcons.layerGroup,
text: context.lang.webxdcChatAppsMenu,
onTap: () => context.navPush(
WebxdcChatAppsView(
groupId: getUUIDforDirectChat(
widget.userId,
userService.currentUser.userId,
),
),
),
),
ContactGroupsSubtitleBuilder( ContactGroupsSubtitleBuilder(
userId: contact.userId, userId: contact.userId,
builder: (context, subtitleWidget) { builder: (context, subtitleWidget) {

View file

@ -23,6 +23,7 @@ import 'package:twonly/src/visual/views/contact/select_contact_groups.view.dart'
import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart';
import 'package:twonly/src/visual/views/groups/group_member.context.dart'; import 'package:twonly/src/visual/views/groups/group_member.context.dart';
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
import 'package:twonly/src/visual/views/webxdc/webxdc_chat_apps.view.dart';
class GroupView extends StatefulWidget { class GroupView extends StatefulWidget {
const GroupView(this.groupId, {super.key}); const GroupView(this.groupId, {super.key});
@ -213,6 +214,13 @@ class _GroupViewState extends State<GroupView> {
groupId: widget.groupId, groupId: widget.groupId,
disabled: !_group!.isGroupAdmin, disabled: !_group!.isGroupAdmin,
), ),
BetterListTile(
icon: FontAwesomeIcons.layerGroup,
text: context.lang.webxdcChatAppsMenu,
onTap: () => context.navPush(
WebxdcChatAppsView(groupId: widget.groupId),
),
),
ContactGroupsSubtitleBuilder( ContactGroupsSubtitleBuilder(
groupId: widget.groupId, groupId: widget.groupId,
builder: (context, subtitleWidget) { builder: (context, subtitleWidget) {

View file

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -12,6 +11,7 @@ import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/elements/pro_badge.element.dart';
class MemoriesCloudBackupPromoComp extends StatelessWidget { class MemoriesCloudBackupPromoComp extends StatelessWidget {
const MemoriesCloudBackupPromoComp({super.key}); const MemoriesCloudBackupPromoComp({super.key});
@ -75,35 +75,7 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( const ProBadge(),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.amber.shade700,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FaIcon(
FontAwesomeIcons.star,
size: 10,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
context.lang.backupCloudProBadge,
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),

View file

@ -15,6 +15,7 @@ import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/elements/pro_badge.element.dart';
import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart';
import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart';
@ -206,39 +207,12 @@ class _BackupViewState extends State<BackupView> {
: '-')), : '-')),
), ),
trailing: isFreePlan trailing: isFreePlan
? Row( ? const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( ProBadge(),
padding: const EdgeInsets.symmetric( SizedBox(width: 8),
horizontal: 6, Switch(
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.amber.shade700,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const FaIcon(
FontAwesomeIcons.star,
size: 10,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
context.lang.backupCloudProBadge,
style: const TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(width: 8),
const Switch(
value: false, value: false,
onChanged: null, onChanged: null,
), ),

View file

@ -376,7 +376,7 @@ class _PlanCardState extends State<PlanCard> {
context.lang.proFeature2, context.lang.proFeature2,
context.lang.proFeature3, context.lang.proFeature3,
context.lang.proFeature4, context.lang.proFeature4,
// context.lang.proFeature4, context.lang.subscriptionAllApps,
]; ];
case 'Family': case 'Family':
features = [ features = [
@ -384,6 +384,7 @@ class _PlanCardState extends State<PlanCard> {
context.lang.familyFeature2, context.lang.familyFeature2,
context.lang.familyFeature3, context.lang.familyFeature3,
context.lang.familyFeature4, context.lang.familyFeature4,
context.lang.subscriptionAllApps,
]; ];
default: default:
} }

View file

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/core/bridge/webxdc.dart' as rust_webxdc;
import 'package:twonly/src/services/webxdc/webxdc.service.dart';
import 'package:twonly/src/services/webxdc/webxdc_host.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
import 'package:twonly/src/visual/views/webxdc/webxdc_app.view.dart';
/// One-time apps already attached to a chat, kept close to its profile.
class WebxdcChatAppsView extends StatefulWidget {
const WebxdcChatAppsView({required this.groupId, super.key});
final String groupId;
@override
State<WebxdcChatAppsView> createState() => _WebxdcChatAppsViewState();
}
class _WebxdcChatAppsViewState extends State<WebxdcChatAppsView> {
List<rust_webxdc.WebxdcOneTimeInstance>? _apps;
bool _loading = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_loading) return;
_loading = true;
_load();
}
Future<void> _load() async {
final apps = await WebxdcService.oneTimeInstances(
widget.groupId,
readerLanguages(context),
);
if (mounted) {
setState(() {
_apps = apps;
_loading = false;
});
}
}
Future<void> _open(rust_webxdc.WebxdcOneTimeInstance app) async {
final messenger = ScaffoldMessenger.of(context);
final unavailable = context.lang.webxdcUnavailable;
final launch = await WebxdcHost.prepare(
app.instanceId,
readerLanguages(context),
);
if (launch.failure != null) {
messenger.showSnackBar(SnackBar(content: Text(unavailable)));
return;
}
if (!mounted) return;
await context.navPush(WebxdcAppView(launch: launch));
await _load();
}
@override
Widget build(BuildContext context) {
final apps = _apps;
return Scaffold(
appBar: AppBar(title: Text(context.lang.webxdcChatAppsTitle)),
body: apps == null
? const Center(child: CircularProgressIndicator())
: apps.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
context.lang.webxdcChatAppsEmpty,
textAlign: TextAlign.center,
),
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: apps.length,
itemBuilder: (context, index) {
final app = apps[index];
final subtitle = app.document ?? app.summary;
return ListTile(
leading: SizedBox(
width: 44,
height: 44,
child: app.icon == null
? const Center(
child: FaIcon(FontAwesomeIcons.puzzlePiece),
)
: ClipRRect(
borderRadius: BorderRadius.circular(9),
child: Image.memory(app.icon!, fit: BoxFit.cover),
),
),
title: Text(app.name),
subtitle: subtitle == null
? null
: Text(
subtitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _open(app),
);
},
),
);
}
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT app_id AS \"app_id!: String\", version AS \"version!: i64\",\n name AS \"name!: String\",\n name_translations AS \"name_translations!: String\",\n source_code_url AS \"source_code_url: String\",\n description AS \"description!: String\",\n icon AS \"icon: Vec<u8>\", bundle_bytes AS \"bundle_bytes!: i64\"\n FROM webxdc_apps\n WHERE published = 1\n AND version = (SELECT MAX(version) FROM webxdc_apps AS newer\n WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1)\n ORDER BY name ASC", "query": "SELECT app_id AS \"app_id!: String\", version AS \"version!: i64\",\n name AS \"name!: String\",\n pro_only AS \"pro_only!: bool\",\n one_time AS \"one_time!: bool\",\n (SELECT instance_id FROM webxdc_instances\n WHERE webxdc_instances.group_id = ?\n AND webxdc_instances.app_id = webxdc_apps.app_id\n ORDER BY created_at ASC LIMIT 1) AS \"instance_id: String\",\n name_translations AS \"name_translations!: String\",\n source_code_url AS \"source_code_url: String\",\n description AS \"description!: String\",\n icon AS \"icon: Vec<u8>\", bundle_bytes AS \"bundle_bytes!: i64\"\n FROM webxdc_apps\n WHERE published = 1\n AND version = (SELECT MAX(version) FROM webxdc_apps AS newer\n WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1)\n ORDER BY sort_order ASC, name ASC, app_id ASC",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -37,8 +37,41 @@
} }
}, },
{ {
"name": "name_translations!: String", "name": "pro_only!: bool",
"ordinal": 3, "ordinal": 3,
"type_info": "Bool",
"origin": {
"Table": {
"table": "webxdc_apps",
"name": "pro_only"
}
}
},
{
"name": "one_time!: bool",
"ordinal": 4,
"type_info": "Bool",
"origin": {
"Table": {
"table": "webxdc_apps",
"name": "one_time"
}
}
},
{
"name": "instance_id: String",
"ordinal": 5,
"type_info": "Text",
"origin": {
"Table": {
"table": "webxdc_instances",
"name": "instance_id"
}
}
},
{
"name": "name_translations!: String",
"ordinal": 6,
"type_info": "Text", "type_info": "Text",
"origin": { "origin": {
"Table": { "Table": {
@ -49,7 +82,7 @@
}, },
{ {
"name": "source_code_url: String", "name": "source_code_url: String",
"ordinal": 4, "ordinal": 7,
"type_info": "Text", "type_info": "Text",
"origin": { "origin": {
"Table": { "Table": {
@ -60,7 +93,7 @@
}, },
{ {
"name": "description!: String", "name": "description!: String",
"ordinal": 5, "ordinal": 8,
"type_info": "Text", "type_info": "Text",
"origin": { "origin": {
"Table": { "Table": {
@ -71,7 +104,7 @@
}, },
{ {
"name": "icon: Vec<u8>", "name": "icon: Vec<u8>",
"ordinal": 6, "ordinal": 9,
"type_info": "Blob", "type_info": "Blob",
"origin": { "origin": {
"Table": { "Table": {
@ -82,7 +115,7 @@
}, },
{ {
"name": "bundle_bytes!: i64", "name": "bundle_bytes!: i64",
"ordinal": 7, "ordinal": 10,
"type_info": "Integer", "type_info": "Integer",
"origin": { "origin": {
"Table": { "Table": {
@ -93,18 +126,21 @@
} }
], ],
"parameters": { "parameters": {
"Right": 0 "Right": 1
}, },
"nullable": [ "nullable": [
false, false,
false, false,
false, false,
false, false,
false,
true,
false,
true, true,
false, false,
true, true,
false false
] ]
}, },
"hash": "b71a58e0499f8b69debddba47cf1ec86c4b99309441a4506e998beedfcead834" "hash": "1eb6b6e8e4ed43f43c3e6c570ffad96638f7c02d4baa4bdea006ea2a439ec24f"
} }

View file

@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE groups\n SET last_message_exchange = MAX(last_message_exchange, ?)\n WHERE group_id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE groups\n SET last_message_exchange = MAX(last_message_exchange, ?),\n deleted_content = 0\n WHERE group_id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "3e9671207184c4df6b3d3c96bf4201160e557039a18115c4aa1737a80ebb5153"
}

View file

@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT instance.instance_id AS \"instance_id!: String\",\n app.name AS \"name!: String\",\n app.name_translations AS \"name_translations!: String\",\n app.icon AS \"icon: Vec<u8>\",\n instance.summary AS \"summary: String\",\n instance.document AS \"document: String\"\n FROM webxdc_instances AS instance\n JOIN webxdc_apps AS app\n ON app.app_id = instance.app_id AND app.version = instance.version\n WHERE instance.group_id = ? AND app.one_time = 1\n ORDER BY instance.last_update_at DESC, instance.created_at DESC",
"describe": {
"columns": [
{
"name": "instance_id!: String",
"ordinal": 0,
"type_info": "Text",
"origin": {
"Table": {
"table": "webxdc_instances",
"name": "instance_id"
}
}
},
{
"name": "name!: String",
"ordinal": 1,
"type_info": "Text",
"origin": {
"Table": {
"table": "webxdc_apps",
"name": "name"
}
}
},
{
"name": "name_translations!: String",
"ordinal": 2,
"type_info": "Text",
"origin": {
"Table": {
"table": "webxdc_apps",
"name": "name_translations"
}
}
},
{
"name": "icon: Vec<u8>",
"ordinal": 3,
"type_info": "Blob",
"origin": {
"Table": {
"table": "webxdc_apps",
"name": "icon"
}
}
},
{
"name": "summary: String",
"ordinal": 4,
"type_info": "Text",
"origin": {
"Table": {
"table": "webxdc_instances",
"name": "summary"
}
}
},
{
"name": "document: String",
"ordinal": 5,
"type_info": "Text",
"origin": {
"Table": {
"table": "webxdc_instances",
"name": "document"
}
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true
]
},
"hash": "7a89aa45a3186a9a4c712976e21d7a4e3c289b449bd5ce432845cbdc02b234a6"
}

View file

@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO webxdc_apps\n (app_id, version, name, name_translations, source_code_url,\n description, icon, bundle_sha256, bundle_bytes, published,\n cached_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)\n ON CONFLICT(app_id, version) DO UPDATE SET\n name = excluded.name,\n name_translations = excluded.name_translations,\n source_code_url = excluded.source_code_url,\n description = excluded.description,\n icon = excluded.icon,\n bundle_sha256 = excluded.bundle_sha256,\n bundle_bytes = excluded.bundle_bytes,\n published = 1,\n cached_at = excluded.cached_at",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "cc6625aa285f9e6dcdc1a73d7bfb6ad23cff6c8932f61b48645c431dd7adaf48"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER), deleted_content = 0 WHERE group_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "cdd3c7feb92de75c85642d5f99d0ac3f8bb715da50ba1fd25484041cd50a2818"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO webxdc_apps\n (app_id, version, name, name_translations, source_code_url,\n description, icon, bundle_sha256, bundle_bytes, published,\n cached_at, pro_only, sort_order, one_time)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)\n ON CONFLICT(app_id, version) DO UPDATE SET\n name = excluded.name,\n name_translations = excluded.name_translations,\n source_code_url = excluded.source_code_url,\n description = excluded.description,\n icon = excluded.icon,\n bundle_sha256 = excluded.bundle_sha256,\n bundle_bytes = excluded.bundle_bytes,\n published = 1,\n pro_only = excluded.pro_only,\n sort_order = excluded.sort_order,\n one_time = excluded.one_time,\n cached_at = excluded.cached_at",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "fa52c200d8a2e33db876b41d77b460d0d697d3818061c59cf7c20106c1a64175"
}

View file

@ -680,6 +680,7 @@ pub(crate) async fn handle_encrypted(
let notification_content = content.clone(); let notification_content = content.clone();
handle_encrypted_inner(ctx, t, from_user_id, receipt_id, content).await?; handle_encrypted_inner(ctx, t, from_user_id, receipt_id, content).await?;
crate::services::notifications::record_incoming_event( crate::services::notifications::record_incoming_event(
ctx,
t, t,
from_user_id, from_user_id,
receipt_id, receipt_id,

View file

@ -17,7 +17,7 @@ use prost::Message as ProstMessage;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
pub(crate) async fn handle_additional_data_message( pub(crate) async fn handle_additional_data_message(
ctx: &Context, ctx: &std::sync::Arc<Context>,
tr: &mut Transaction<'_, Sqlite>, tr: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
group_id: &str, group_id: &str,
@ -40,7 +40,20 @@ pub(crate) async fn handle_additional_data_message(
}; };
if let Some(data) = data.as_ref() { if let Some(data) = data.as_ref() {
if let Some(update) = data.webxdc_update.as_ref() { if data.webxdc_sync_request.is_some() {
let ctx = ctx.clone();
let group_id = group_id.to_owned();
tokio::spawn(async move {
if let Err(error) = WebxdcService::new(&ctx)
.send_one_time_apps_to_contact(&group_id, from_user_id)
.await
{
tracing::warn!(%error, "could not answer one-time app sync request");
}
});
} else if let Some(sync) = data.webxdc_sync.as_ref() {
WebxdcService::handle_sync_chunk(tr, group_id, from_user_id, sync).await?;
} else if let Some(update) = data.webxdc_update.as_ref() {
// App state belongs in the instance's own log, which is ordered, // App state belongs in the instance's own log, which is ordered,
// gap free, and outside the reach of the chat's deletion timer. // gap free, and outside the reach of the chat's deletion timer.
WebxdcService::handle_incoming_update( WebxdcService::handle_incoming_update(

View file

@ -92,6 +92,11 @@ pub(crate) async fn handle_group_create(
.await?; .await?;
GroupService::spawn_state_refresh_and_announce(ctx, group_id.to_owned()); GroupService::spawn_state_refresh_and_announce(ctx, group_id.to_owned());
crate::services::webxdc::WebxdcService::spawn_sync_request(
ctx,
group_id.to_owned(),
from_user_id,
);
Ok(()) Ok(())
} }

View file

@ -133,8 +133,13 @@ message WebxdcCatalogEntry {
// The name in every language the bundle carries it in, keyed the same way. // The name in every language the bundle carries it in, keyed the same way.
// `name` is what a client shows when none of them is a language it reads. // `name` is what a client shows when none of them is a language it reads.
map<string, string> name_translations = 10; map<string, string> name_translations = 10;
bool pro_only = 11;
// Only one instance of this app may be placed into a chat.
bool one_time = 12;
} }
message WebxdcCatalog { message WebxdcCatalog {
// Admin-defined app order, with versions of each app grouped newest first.
// Clients retain this order when caching and displaying the drawer.
repeated WebxdcCatalogEntry entries = 1; repeated WebxdcCatalogEntry entries = 1;
} }

View file

@ -15,6 +15,8 @@ message AdditionalMessageData {
WEBXDC_APP = 4; WEBXDC_APP = 4;
WEBXDC_UPDATE = 5; WEBXDC_UPDATE = 5;
WEBXDC_SENT = 6; WEBXDC_SENT = 6;
WEBXDC_SYNC = 7;
WEBXDC_SYNC_REQUEST = 8;
} }
Type type = 1; Type type = 1;
@ -25,6 +27,45 @@ message AdditionalMessageData {
optional WebxdcApp webxdc_app = 6; optional WebxdcApp webxdc_app = 6;
optional WebxdcUpdate webxdc_update = 7; optional WebxdcUpdate webxdc_update = 7;
optional WebxdcOrigin webxdc_origin = 8; optional WebxdcOrigin webxdc_origin = 8;
optional WebxdcSyncChunk webxdc_sync = 9;
optional WebxdcSyncRequest webxdc_sync_request = 10;
}
// A peer-to-peer transfer of existing one-time app state. The encoded payload
// is split into bounded hidden messages and installed only after every chunk
// arrived and the digest matches.
message WebxdcSyncChunk {
string transfer_id = 1;
uint32 chunk_index = 2;
uint32 chunk_count = 3;
bytes payload_sha256 = 4;
bytes payload = 5;
}
message WebxdcSyncRequest {}
message WebxdcSyncPayload {
repeated WebxdcSyncInstance instances = 1;
}
message WebxdcSyncInstance {
string instance_id = 1;
string app_id = 2;
int64 version = 3;
optional string summary = 4;
optional string document = 5;
int64 created_at = 6;
int64 last_update_at = 7;
repeated WebxdcSyncUpdate updates = 8;
}
message WebxdcSyncUpdate {
string message_id = 1;
optional int64 sender_id = 2;
string payload = 3;
optional string info = 4;
optional string href = 5;
int64 received_at = 6;
} }
// Attached to a message a webxdc app asked the user to send, so the chat can // Attached to a message a webxdc app asked the user to send, so the chat can
@ -57,4 +98,7 @@ message WebxdcUpdate {
optional string href = 4; optional string href = 4;
optional string summary = 5; optional string summary = 5;
optional string document = 6; optional string document = 6;
} // JSON map of app-scoped member addresses to notification text.
// Absent preserves legacy broadcast; an empty map notifies nobody.
optional string notify = 7;
}

View file

@ -126,9 +126,9 @@ impl Server {
ResponseOk::PasswordlessNotificationMessages(msgs) => Some(msgs), ResponseOk::PasswordlessNotificationMessages(msgs) => Some(msgs),
// The server answers with `None` whenever the poll finds no unseen // The server answers with `None` whenever the poll finds no unseen
// message, which is the normal outcome once every share arrived. // message, which is the normal outcome once every share arrived.
ResponseOk::None(_) => Some( ResponseOk::None(_) => {
proto::server_to_client::response::PasswordlessNotificationMessages::default(), Some(proto::server_to_client::response::PasswordlessNotificationMessages::default())
), }
_ => None, _ => None,
}) })
} }

View file

@ -23,6 +23,18 @@ pub struct WebxdcStoreApp {
pub source_code_url: Option<String>, pub source_code_url: Option<String>,
pub icon: Option<Vec<u8>>, pub icon: Option<Vec<u8>>,
pub bundle_bytes: i64, pub bundle_bytes: i64,
pub pro_only: bool,
pub one_time: bool,
/// The existing instance in this chat when a one-time app was already placed.
pub instance_id: Option<String>,
}
pub struct WebxdcOneTimeInstance {
pub instance_id: String,
pub name: String,
pub icon: Option<Vec<u8>>,
pub summary: Option<String>,
pub document: Option<String>,
} }
pub struct WebxdcInstanceInfo { pub struct WebxdcInstanceInfo {
@ -66,12 +78,18 @@ pub async fn refresh_catalog() -> Result<()> {
/// `languages` is what the UI prefers, most preferred first. Names and /// `languages` is what the UI prefers, most preferred first. Names and
/// descriptions are cached in every language the catalog carries and picked /// descriptions are cached in every language the catalog carries and picked
/// here, so which language a user reads is never sent anywhere. /// here, so which language a user reads is never sent anywhere.
pub async fn catalog(languages: Vec<String>) -> Result<Vec<WebxdcStoreApp>> { pub async fn catalog(group_id: String, languages: Vec<String>) -> Result<Vec<WebxdcStoreApp>> {
let ctx = crate::context::Context::get_static()?; let ctx = crate::context::Context::get_static()?;
let database = ctx.app_db.read().await.clone(); let database = ctx.app_db.read().await.clone();
let rows = sqlx::query!( let rows = sqlx::query!(
r#"SELECT app_id AS "app_id!: String", version AS "version!: i64", r#"SELECT app_id AS "app_id!: String", version AS "version!: i64",
name AS "name!: String", name AS "name!: String",
pro_only AS "pro_only!: bool",
one_time AS "one_time!: bool",
(SELECT instance_id FROM webxdc_instances
WHERE webxdc_instances.group_id = ?
AND webxdc_instances.app_id = webxdc_apps.app_id
ORDER BY created_at ASC LIMIT 1) AS "instance_id: String",
name_translations AS "name_translations!: String", name_translations AS "name_translations!: String",
source_code_url AS "source_code_url: String", source_code_url AS "source_code_url: String",
description AS "description!: String", description AS "description!: String",
@ -80,11 +98,12 @@ pub async fn catalog(languages: Vec<String>) -> Result<Vec<WebxdcStoreApp>> {
WHERE published = 1 WHERE published = 1
AND version = (SELECT MAX(version) FROM webxdc_apps AS newer AND version = (SELECT MAX(version) FROM webxdc_apps AS newer
WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1) WHERE newer.app_id = webxdc_apps.app_id AND newer.published = 1)
ORDER BY name ASC"# ORDER BY sort_order ASC, name ASC, app_id ASC"#,
group_id,
) )
.fetch_all(&database.pool) .fetch_all(&database.pool)
.await?; .await?;
let mut apps: Vec<WebxdcStoreApp> = rows let apps: Vec<WebxdcStoreApp> = rows
.into_iter() .into_iter()
.map(|row| WebxdcStoreApp { .map(|row| WebxdcStoreApp {
app_id: row.app_id, app_id: row.app_id,
@ -103,14 +122,54 @@ pub async fn catalog(languages: Vec<String>) -> Result<Vec<WebxdcStoreApp>> {
source_code_url: row.source_code_url, source_code_url: row.source_code_url,
icon: row.icon, icon: row.icon,
bundle_bytes: row.bundle_bytes, bundle_bytes: row.bundle_bytes,
pro_only: row.pro_only,
one_time: row.one_time,
instance_id: row.instance_id,
}) })
.collect(); .collect();
// The query orders by the untranslated name, which is not the order the // Keep the server order even when names are localized for the reader.
// list is read in once the names are the reader's.
apps.sort_by_key(|app| app.name.to_lowercase());
Ok(apps) Ok(apps)
} }
/// One-time apps already placed into a chat, for the profile shortcut.
pub async fn one_time_instances(
group_id: String,
languages: Vec<String>,
) -> Result<Vec<WebxdcOneTimeInstance>> {
let ctx = crate::context::Context::get_static()?;
let database = ctx.app_db.read().await.clone();
let rows = sqlx::query!(
r#"SELECT instance.instance_id AS "instance_id!: String",
app.name AS "name!: String",
app.name_translations AS "name_translations!: String",
app.icon AS "icon: Vec<u8>",
instance.summary AS "summary: String",
instance.document AS "document: String"
FROM webxdc_instances AS instance
JOIN webxdc_apps AS app
ON app.app_id = instance.app_id AND app.version = instance.version
WHERE instance.group_id = ? AND app.one_time = 1
ORDER BY instance.last_update_at DESC, instance.created_at DESC"#,
group_id,
)
.fetch_all(&database.pool)
.await?;
Ok(rows
.into_iter()
.map(|row| WebxdcOneTimeInstance {
instance_id: row.instance_id,
name: crate::services::webxdc::store::pick_localized(
&row.name_translations,
&languages,
)
.unwrap_or(row.name),
icon: row.icon,
summary: row.summary,
document: row.document,
})
.collect())
}
/// Places an app into a chat and returns the id of the message that carries it. /// Places an app into a chat and returns the id of the message that carries it.
pub async fn create_instance(group_id: String, app_id: String, version: i64) -> Result<String> { pub async fn create_instance(group_id: String, app_id: String, version: i64) -> Result<String> {
let ctx = crate::context::Context::get_static()?; let ctx = crate::context::Context::get_static()?;
@ -135,8 +194,8 @@ pub async fn instance(instance_id: String) -> Result<Option<WebxdcInstanceInfo>>
})) }))
} }
/// Downloads and verifies the bundle if it is not already on disk, and returns /// Opens cached code, adopting an update only if already downloaded.
/// where it landed. Called when the user starts an app, never on message /// Downloads and verifies the pinned bundle only when it is missing. Called when the user starts an app, never on message
/// arrival: a message must not be able to make a device fetch anything. /// arrival: a message must not be able to make a device fetch anything.
pub async fn prepare_bundle(instance_id: String) -> Result<String> { pub async fn prepare_bundle(instance_id: String) -> Result<String> {
let ctx = crate::context::Context::get_static()?; let ctx = crate::context::Context::get_static()?;
@ -144,6 +203,13 @@ pub async fn prepare_bundle(instance_id: String) -> Result<String> {
Ok(path.display().to_string()) Ok(path.display().to_string())
} }
/// Background update check after a webxdc app closes. Downloads only; the
/// running version changes on a later prepare_bundle call, never mid-session.
pub async fn cache_update(instance_id: String) -> Result<()> {
let ctx = crate::context::Context::get_static()?;
WebxdcService::new(ctx).cache_update(&instance_id).await
}
/// Answers one request the webview made for a file inside the bundle. /// Answers one request the webview made for a file inside the bundle.
/// ///
/// The platform shim has already established that the request came from this /// The platform shim has already established that the request came from this
@ -153,7 +219,7 @@ pub async fn serve(instance_id: String, request_path: String) -> Result<WebxdcRe
let service = WebxdcService::new(ctx); let service = WebxdcService::new(ctx);
// The bundle the instance is pinned to, not whatever the store offers now: // The bundle the instance is pinned to, not whatever the store offers now:
// every file of one run comes out of the same `.xdc`, and the store was // every file of one run comes out of the same `.xdc`, and the store was
// already consulted when the app started. // never changed by a background update check.
let path = service.pinned_bundle(&instance_id).await?; let path = service.pinned_bundle(&instance_id).await?;
let init = service.init_script_values(&instance_id).await?; let init = service.init_script_values(&instance_id).await?;
let response = bundle::serve(&path, &request_path, &init); let response = bundle::serve(&path, &request_path, &init);
@ -190,10 +256,11 @@ pub async fn send_update(
href: Option<String>, href: Option<String>,
summary: Option<String>, summary: Option<String>,
document: Option<String>, document: Option<String>,
notify: Option<String>,
) -> Result<()> { ) -> Result<()> {
let ctx = crate::context::Context::get_static()?; let ctx = crate::context::Context::get_static()?;
WebxdcService::new(ctx) WebxdcService::new(ctx)
.send_update(instance_id, payload, info, href, summary, document) .send_update(instance_id, payload, info, href, summary, document, notify)
.await .await
} }
@ -211,3 +278,9 @@ pub async fn delete_instance(instance_id: String) -> Result<Option<String>> {
let ctx = crate::context::Context::get_static()?; let ctx = crate::context::Context::get_static()?;
WebxdcService::new(ctx).delete_instance(&instance_id).await WebxdcService::new(ctx).delete_instance(&instance_id).await
} }
/// Current members of the instance's chat; no account identifiers leave Rust.
pub async fn members(instance_id: String) -> Result<String> {
let ctx = crate::context::Context::get_static()?;
WebxdcService::new(ctx).members(&instance_id).await
}

View file

@ -0,0 +1 @@
ALTER TABLE webxdc_apps ADD COLUMN pro_only BOOLEAN NOT NULL DEFAULT FALSE;

View file

@ -0,0 +1,2 @@
-- Retain the server catalog order when showing cached apps offline.
ALTER TABLE webxdc_apps ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;

View file

@ -0,0 +1,2 @@
-- Apps such as a shared expense ledger have one instance per chat.
ALTER TABLE webxdc_apps ADD COLUMN one_time BOOLEAN NOT NULL DEFAULT FALSE;

View file

@ -0,0 +1,34 @@
-- Chunks survive process restarts and out-of-order delivery. They are removed
-- as soon as a complete, verified state transfer has been installed.
CREATE TABLE webxdc_sync_chunks (
transfer_id TEXT NOT NULL,
group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
sender_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
chunk_index INTEGER NOT NULL,
chunk_count INTEGER NOT NULL,
payload_sha256 BLOB NOT NULL,
payload BLOB NOT NULL,
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
PRIMARY KEY (transfer_id, sender_id, chunk_index)
);
CREATE INDEX idx_webxdc_sync_chunks_created_at
ON webxdc_sync_chunks(created_at);
-- A newly joined member can receive a live update before the instance snapshot.
-- Keep it until the app card arrives instead of dropping shared state.
CREATE TABLE webxdc_pending_updates (
message_id TEXT NOT NULL PRIMARY KEY,
instance_id TEXT NOT NULL,
group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
sender_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
payload TEXT NOT NULL,
info TEXT,
href TEXT,
summary TEXT,
document TEXT,
received_at INTEGER NOT NULL
);
CREATE INDEX idx_webxdc_pending_updates_instance
ON webxdc_pending_updates(instance_id, received_at);

View file

@ -330,7 +330,8 @@ impl Group {
sqlx::query!( sqlx::query!(
r#" r#"
UPDATE groups UPDATE groups
SET last_message_exchange = MAX(last_message_exchange, ?) SET last_message_exchange = MAX(last_message_exchange, ?),
deleted_content = 0
WHERE group_id = ? WHERE group_id = ?
"#, "#,
timestamp, timestamp,

View file

@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi, default_rust_auto_opaque = RustAutoOpaqueMoi,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1362207365; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1591152700;
// Section: executor // Section: executor
@ -161,6 +161,43 @@ fn wire__crate__bridge__webxdc__address_for_impl(
}, },
) )
} }
fn wire__crate__bridge__webxdc__cache_update_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: "cache_update",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_instance_id = <String>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok =
crate::bridge::webxdc::cache_update(api_instance_id).await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__webxdc__catalog_impl( fn wire__crate__bridge__webxdc__catalog_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -183,12 +220,14 @@ fn wire__crate__bridge__webxdc__catalog_impl(
}; };
let mut deserializer = let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message); flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_group_id = <String>::sse_decode(&mut deserializer);
let api_languages = <Vec<String>>::sse_decode(&mut deserializer); let api_languages = <Vec<String>>::sse_decode(&mut deserializer);
deserializer.end(); deserializer.end();
move |context| async move { move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move { (move || async move {
let output_ok = crate::bridge::webxdc::catalog(api_languages).await?; let output_ok =
crate::bridge::webxdc::catalog(api_group_id, api_languages).await?;
Ok(output_ok) Ok(output_ok)
})() })()
.await, .await,
@ -820,6 +859,81 @@ fn wire__crate__bridge__groups__manage_admin_state_impl(
}, },
) )
} }
fn wire__crate__bridge__webxdc__members_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: "members",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_instance_id = <String>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok = crate::bridge::webxdc::members(api_instance_id).await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__webxdc__one_time_instances_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: "one_time_instances",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_group_id = <String>::sse_decode(&mut deserializer);
let api_languages = <Vec<String>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok =
crate::bridge::webxdc::one_time_instances(api_group_id, api_languages)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__webxdc__prepare_bundle_impl( fn wire__crate__bridge__webxdc__prepare_bundle_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -5592,6 +5706,7 @@ fn wire__crate__bridge__webxdc__send_update_impl(
let api_href = <Option<String>>::sse_decode(&mut deserializer); let api_href = <Option<String>>::sse_decode(&mut deserializer);
let api_summary = <Option<String>>::sse_decode(&mut deserializer); let api_summary = <Option<String>>::sse_decode(&mut deserializer);
let api_document = <Option<String>>::sse_decode(&mut deserializer); let api_document = <Option<String>>::sse_decode(&mut deserializer);
let api_notify = <Option<String>>::sse_decode(&mut deserializer);
deserializer.end(); deserializer.end();
move |context| async move { move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
@ -5603,6 +5718,7 @@ fn wire__crate__bridge__webxdc__send_update_impl(
api_href, api_href,
api_summary, api_summary,
api_document, api_document,
api_notify,
) )
.await?; .await?;
Ok(output_ok) Ok(output_ok)
@ -6671,6 +6787,20 @@ impl SseDecode for Vec<crate::database::app::SqlValue> {
} }
} }
impl SseDecode for Vec<crate::bridge::webxdc::WebxdcOneTimeInstance> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut len_ = <i32>::sse_decode(deserializer);
let mut ans_ = Vec::with_capacity(len_ as usize);
for idx_ in 0..len_ {
ans_.push(<crate::bridge::webxdc::WebxdcOneTimeInstance>::sse_decode(
deserializer,
));
}
return ans_;
}
}
impl SseDecode for Vec<crate::bridge::webxdc::WebxdcStoreApp> { impl SseDecode for Vec<crate::bridge::webxdc::WebxdcStoreApp> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -7330,6 +7460,24 @@ impl SseDecode for crate::bridge::webxdc::WebxdcInstanceInfo {
} }
} }
impl SseDecode for crate::bridge::webxdc::WebxdcOneTimeInstance {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_instanceId = <String>::sse_decode(deserializer);
let mut var_name = <String>::sse_decode(deserializer);
let mut var_icon = <Option<Vec<u8>>>::sse_decode(deserializer);
let mut var_summary = <Option<String>>::sse_decode(deserializer);
let mut var_document = <Option<String>>::sse_decode(deserializer);
return crate::bridge::webxdc::WebxdcOneTimeInstance {
instance_id: var_instanceId,
name: var_name,
icon: var_icon,
summary: var_summary,
document: var_document,
};
}
}
impl SseDecode for crate::bridge::webxdc::WebxdcResponse { impl SseDecode for crate::bridge::webxdc::WebxdcResponse {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -7358,6 +7506,9 @@ impl SseDecode for crate::bridge::webxdc::WebxdcStoreApp {
let mut var_sourceCodeUrl = <Option<String>>::sse_decode(deserializer); let mut var_sourceCodeUrl = <Option<String>>::sse_decode(deserializer);
let mut var_icon = <Option<Vec<u8>>>::sse_decode(deserializer); let mut var_icon = <Option<Vec<u8>>>::sse_decode(deserializer);
let mut var_bundleBytes = <i64>::sse_decode(deserializer); let mut var_bundleBytes = <i64>::sse_decode(deserializer);
let mut var_proOnly = <bool>::sse_decode(deserializer);
let mut var_oneTime = <bool>::sse_decode(deserializer);
let mut var_instanceId = <Option<String>>::sse_decode(deserializer);
return crate::bridge::webxdc::WebxdcStoreApp { return crate::bridge::webxdc::WebxdcStoreApp {
app_id: var_appId, app_id: var_appId,
version: var_version, version: var_version,
@ -7366,6 +7517,9 @@ impl SseDecode for crate::bridge::webxdc::WebxdcStoreApp {
source_code_url: var_sourceCodeUrl, source_code_url: var_sourceCodeUrl,
icon: var_icon, icon: var_icon,
bundle_bytes: var_bundleBytes, bundle_bytes: var_bundleBytes,
pro_only: var_proOnly,
one_time: var_oneTime,
instance_id: var_instanceId,
}; };
} }
} }
@ -7400,163 +7554,166 @@ fn pde_ffi_dispatcher_primary_impl(
1 => wire__crate__bridge__groups__add_hidden_contact_impl(port, ptr, rust_vec_len, data_len), 1 => wire__crate__bridge__groups__add_hidden_contact_impl(port, ptr, rust_vec_len, data_len),
2 => wire__crate__bridge__groups__add_new_group_members_impl(port, ptr, rust_vec_len, data_len), 2 => wire__crate__bridge__groups__add_new_group_members_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__bridge__webxdc__address_for_impl(port, ptr, rust_vec_len, data_len), 3 => wire__crate__bridge__webxdc__address_for_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len), 4 => wire__crate__bridge__webxdc__cache_update_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len), 5 => wire__crate__bridge__webxdc__catalog_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len), 6 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len), 7 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), 8 => wire__crate__bridge__webxdc__create_instance_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len), 9 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), 10 => wire__crate__bridge__webxdc__delete_instance_impl(port, ptr, rust_vec_len, data_len),
11 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), 11 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len),
12 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), 12 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), 13 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len),
14 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), 14 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), 15 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), 16 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), 17 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), 18 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len),
19 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len), 19 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), 20 => wire__crate__bridge__webxdc__instance_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len), 21 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), 22 => wire__crate__bridge__logging__load_log_file_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len), 23 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len), 24 => wire__crate__bridge__webxdc__members_impl(port, ptr, rust_vec_len, data_len),
25 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len), 25 => wire__crate__bridge__webxdc__one_time_instances_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), 26 => wire__crate__bridge__webxdc__prepare_bundle_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len), 27 => wire__crate__bridge__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), 28 => wire__crate__bridge__webxdc__refresh_catalog_impl(port, ptr, rust_vec_len, data_len),
29 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), 29 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len),
31 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len), 30 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), 31 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), 32 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), 34 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), 36 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), 37 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), 38 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), 39 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), 40 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), 41 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), 42 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), 43 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), 44 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), 45 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len), 46 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), 48 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), 49 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), 50 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len),
51 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), 51 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), 52 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), 53 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), 54 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), 55 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len),
56 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), 56 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len),
57 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), 57 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
58 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), 58 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len),
59 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), 59 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
60 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), 60 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
61 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), 61 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
62 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), 62 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
63 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), 63 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
64 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), 64 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
65 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len), 65 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
66 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), 66 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
67 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), 67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
68 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), 68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
69 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), 69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
70 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), 70 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
71 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), 71 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
72 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), 72 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
73 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), 73 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
74 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), 74 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
75 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), 75 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
76 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), 76 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
77 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), 77 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
78 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len), 78 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
79 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), 79 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
80 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len), 80 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
81 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len), 81 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
82 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), 82 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
83 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len), 83 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
84 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), 84 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
85 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), 85 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
86 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), 86 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
87 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), 87 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
88 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), 88 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
89 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), 89 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
90 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), 90 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
91 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), 91 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
92 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), 92 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
93 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), 93 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
94 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), 94 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
95 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), 95 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
96 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), 96 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
97 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), 97 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
98 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), 98 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
99 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), 99 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
100 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), 100 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
101 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), 101 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
102 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), 102 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
103 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), 103 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
104 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), 104 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
105 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), 105 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
106 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), 106 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
107 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len), 107 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
108 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), 108 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
109 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), 109 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
110 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), 110 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
111 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len), 111 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
112 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), 112 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
113 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), 113 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
114 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), 114 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
115 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len), 115 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
116 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), 116 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
117 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), 117 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
118 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len), 118 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
119 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), 119 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
120 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), 120 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
121 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), 121 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
122 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), 122 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
123 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), 123 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
124 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), 124 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
125 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), 125 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
126 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), 126 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
127 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), 127 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
128 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), 128 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
129 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), 129 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
130 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), 130 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
131 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 131 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
132 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), 132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
133 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 133 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
134 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), 134 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
136 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), 136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
137 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), 137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
138 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), 138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
139 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), 139 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
140 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), 140 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), 141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len), 142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), 143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), 144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
145 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), 145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
146 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), 146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
147 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), 147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
148 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), 148 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
149 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), 149 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
150 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), 150 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
151 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), 151 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
152 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), 152 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
153 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), 153 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
154 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len), 154 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
155 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len), 155 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
156 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), 156 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
157 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), 157 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
158 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len), 158 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
160 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), 159 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
161 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), 160 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
162 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), 161 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
163 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), 163 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
164 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), 164 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
165 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
166 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
167 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -7569,17 +7726,17 @@ fn pde_ffi_dispatcher_sync_impl(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs // Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id { match func_id {
30 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), 33 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
32 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), 35 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len),
44 => { 47 => {
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
} }
159 => wire__crate__bridge__user_config__user_config_api_clone_impl( 162 => wire__crate__bridge__user_config__user_config_api_clone_impl(
ptr, ptr,
rust_vec_len, rust_vec_len,
data_len, data_len,
), ),
165 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), 168 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -8528,6 +8685,30 @@ impl flutter_rust_bridge::IntoIntoDart<crate::bridge::webxdc::WebxdcInstanceInfo
} }
} }
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::webxdc::WebxdcOneTimeInstance {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.instance_id.into_into_dart().into_dart(),
self.name.into_into_dart().into_dart(),
self.icon.into_into_dart().into_dart(),
self.summary.into_into_dart().into_dart(),
self.document.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::webxdc::WebxdcOneTimeInstance
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::webxdc::WebxdcOneTimeInstance>
for crate::bridge::webxdc::WebxdcOneTimeInstance
{
fn into_into_dart(self) -> crate::bridge::webxdc::WebxdcOneTimeInstance {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::webxdc::WebxdcResponse { impl flutter_rust_bridge::IntoDart for crate::bridge::webxdc::WebxdcResponse {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
@ -8562,6 +8743,9 @@ impl flutter_rust_bridge::IntoDart for crate::bridge::webxdc::WebxdcStoreApp {
self.source_code_url.into_into_dart().into_dart(), self.source_code_url.into_into_dart().into_dart(),
self.icon.into_into_dart().into_dart(), self.icon.into_into_dart().into_dart(),
self.bundle_bytes.into_into_dart().into_dart(), self.bundle_bytes.into_into_dart().into_dart(),
self.pro_only.into_into_dart().into_dart(),
self.one_time.into_into_dart().into_dart(),
self.instance_id.into_into_dart().into_dart(),
] ]
.into_dart() .into_dart()
} }
@ -9047,6 +9231,16 @@ impl SseEncode for Vec<crate::database::app::SqlValue> {
} }
} }
impl SseEncode for Vec<crate::bridge::webxdc::WebxdcOneTimeInstance> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(self.len() as _, serializer);
for item in self {
<crate::bridge::webxdc::WebxdcOneTimeInstance>::sse_encode(item, serializer);
}
}
}
impl SseEncode for Vec<crate::bridge::webxdc::WebxdcStoreApp> { impl SseEncode for Vec<crate::bridge::webxdc::WebxdcStoreApp> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -9570,6 +9764,17 @@ impl SseEncode for crate::bridge::webxdc::WebxdcInstanceInfo {
} }
} }
impl SseEncode for crate::bridge::webxdc::WebxdcOneTimeInstance {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.instance_id, serializer);
<String>::sse_encode(self.name, serializer);
<Option<Vec<u8>>>::sse_encode(self.icon, serializer);
<Option<String>>::sse_encode(self.summary, serializer);
<Option<String>>::sse_encode(self.document, serializer);
}
}
impl SseEncode for crate::bridge::webxdc::WebxdcResponse { impl SseEncode for crate::bridge::webxdc::WebxdcResponse {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -9591,6 +9796,9 @@ impl SseEncode for crate::bridge::webxdc::WebxdcStoreApp {
<Option<String>>::sse_encode(self.source_code_url, serializer); <Option<String>>::sse_encode(self.source_code_url, serializer);
<Option<Vec<u8>>>::sse_encode(self.icon, serializer); <Option<Vec<u8>>>::sse_encode(self.icon, serializer);
<i64>::sse_encode(self.bundle_bytes, serializer); <i64>::sse_encode(self.bundle_bytes, serializer);
<bool>::sse_encode(self.pro_only, serializer);
<bool>::sse_encode(self.one_time, serializer);
<Option<String>>::sse_encode(self.instance_id, serializer);
} }
} }

View file

@ -39,7 +39,7 @@ impl MessageService {
|| content.media.is_some() || content.media.is_some()
|| content.text_message.is_some() || content.text_message.is_some()
{ {
sqlx::query!("UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ?", group_id) sqlx::query!("UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER), deleted_content = 0 WHERE group_id = ?", group_id)
.execute(&database.pool).await?; .execute(&database.pool).await?;
} }
let members = sqlx::query_scalar!( let members = sqlx::query_scalar!(
@ -412,6 +412,8 @@ impl MessageService {
webxdc_app: None, webxdc_app: None,
webxdc_update: None, webxdc_update: None,
webxdc_origin: None, webxdc_origin: None,
webxdc_sync: None,
webxdc_sync_request: None,
} }
.encode_to_vec(); .encode_to_vec();
self.insert_and_send_additional_data(group_id, "contacts".into(), data, false) self.insert_and_send_additional_data(group_id, "contacts".into(), data, false)
@ -447,6 +449,8 @@ impl MessageService {
webxdc_app: None, webxdc_app: None,
webxdc_update: None, webxdc_update: None,
webxdc_origin: None, webxdc_origin: None,
webxdc_sync: None,
webxdc_sync_request: None,
} }
.encode_to_vec(); .encode_to_vec();
self.insert_and_send_additional_data(group_id, "askAboutUser".into(), data, false) self.insert_and_send_additional_data(group_id, "askAboutUser".into(), data, false)

View file

@ -31,12 +31,30 @@ static DE_TRANSLATIONS: LazyLock<HashMap<String, String>> =
/// The update itself is hidden -- app state rather than something a person /// The update itself is hidden -- app state rather than something a person
/// sent -- but an `info` is written to be read, and becomes a chat row of its /// sent -- but an `info` is written to be read, and becomes a chat row of its
/// own on arrival. That row is what the receiver is woken and notified for. /// own on arrival. That row is what the receiver is woken and notified for.
fn webxdc_announcement(message: &encrypted_content::AdditionalDataMessage) -> Option<String> { fn webxdc_announcement(
message: &encrypted_content::AdditionalDataMessage,
user_id: i64,
) -> Option<String> {
let data = <proto::AdditionalMessageData as prost::Message>::decode( let data = <proto::AdditionalMessageData as prost::Message>::decode(
message.additional_message_data.as_deref()?, message.additional_message_data.as_deref()?,
) )
.ok()?; .ok()?;
crate::services::webxdc::announcement(&data.webxdc_update?) let update = data.webxdc_update?;
// Keep chat announcements shared, but only wake recipients selected by the app.
let announcement = crate::services::webxdc::announcement(&update)?;
match update.notify.as_deref() {
None => Some(announcement),
Some(raw) => {
let recipients: std::collections::BTreeMap<String, String> =
serde_json::from_str(raw).ok()?;
let address =
crate::services::webxdc::WebxdcService::address_for(&update.instance_id, user_id);
let text = recipients.get(&address)?;
let mut notification = update;
notification.info = Some(text.clone());
crate::services::webxdc::announcement(&notification)
}
}
} }
/// Who wrote the message with this id, as this device has it. /// Who wrote the message with this id, as this device has it.
@ -85,7 +103,9 @@ pub(crate) async fn should_wake_receiver(
|| content || content
.additional_data_message .additional_data_message
.as_ref() .as_ref()
.is_some_and(|message| !message.hidden || webxdc_announcement(message).is_some()) .is_some_and(|message| {
!message.hidden || webxdc_announcement(message, target_user_id).is_some()
})
|| content.group_create.is_some() || content.group_create.is_some()
|| content.media.as_ref().is_some_and(|value| { || content.media.as_ref().is_some_and(|value| {
// Widget media lands already opened and is deliberately recorded // Widget media lands already opened and is deliberately recorded
@ -199,6 +219,7 @@ impl NotificationDraft {
/// transaction that commits that message. Transport retries are deduplicated /// transaction that commits that message. Transport retries are deduplicated
/// by the receipt-derived event ID. /// by the receipt-derived event ID.
pub(crate) async fn record_incoming_event( pub(crate) async fn record_incoming_event(
ctx: &Context,
transaction: &mut Transaction<'_, Sqlite>, transaction: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
receipt_id: &str, receipt_id: &str,
@ -270,9 +291,10 @@ pub(crate) async fn record_incoming_event(
// announcement -- and only once the row it materialised into exists, // announcement -- and only once the row it materialised into exists,
// which it does not when the update was dropped for an unknown // which it does not when the update was dropped for an unknown
// instance, a mismatched chat, or an app that is over budget. // instance, a mismatched chat, or an app that is over budget.
let self_id = UserConfig::load_required_from(ctx)?.user_id;
let announcement = message let announcement = message
.hidden .hidden
.then(|| webxdc_announcement(message)) .then(|| webxdc_announcement(message, self_id))
.flatten(); .flatten();
let message_id = if message.hidden { let message_id = if message.hidden {
crate::services::webxdc::WebxdcService::info_message_id(&message.sender_message_id) crate::services::webxdc::WebxdcService::info_message_id(&message.sender_message_id)
@ -842,7 +864,7 @@ mod tests {
} }
fn announcement_of(content: &proto::EncryptedContent) -> Option<String> { fn announcement_of(content: &proto::EncryptedContent) -> Option<String> {
webxdc_announcement(content.additional_data_message.as_ref().unwrap()) webxdc_announcement(content.additional_data_message.as_ref().unwrap(), 42)
} }
#[test] #[test]
@ -857,6 +879,62 @@ mod tests {
assert!(announcement_of(&webxdc_update(Some(" \u{200e} "))).is_none()); assert!(announcement_of(&webxdc_update(Some(" \u{200e} "))).is_none());
} }
fn targeted_update(notify: &str) -> proto::EncryptedContent {
let mut content = webxdc_update(Some("Dinner added"));
let message = content.additional_data_message.as_mut().unwrap();
let mut data = <proto::AdditionalMessageData as prost::Message>::decode(
message.additional_message_data.as_deref().unwrap(),
)
.unwrap();
data.webxdc_update.as_mut().unwrap().notify = Some(notify.to_owned());
message.additional_message_data = Some(prost::Message::encode_to_vec(&data));
content
}
#[tokio::test]
async fn expense_notifications_only_wake_and_record_affected_members() -> Result<()> {
let directory = tempfile::tempdir()?;
let ctx =
Context::init_for_testing(directory.path().join("db"), directory.path().join("data"))
.await?;
UserConfig::save_json(
&ctx,
r#"{"userId":42,"username":"ben","displayName":"Ben"}"#,
)?;
let database = ctx.app_db.read().await.clone();
let mut tx = database.pool.begin().await?;
sqlx::query("INSERT INTO contacts(user_id, username) VALUES (7, 'anna')")
.execute(&mut *tx)
.await?;
sqlx::query("INSERT INTO groups(group_id, group_name) VALUES ('g', 'Trip')")
.execute(&mut *tx)
.await?;
sqlx::query("INSERT INTO messages(message_id, group_id, sender_id, type, content) VALUES ('message-info', 'g', 7, 'text', 'Dinner added')").execute(&mut *tx).await?;
let address = crate::services::webxdc::WebxdcService::address_for("instance", 42);
let raw = serde_json::json!({address: "You share dinner: €20"}).to_string();
let content = targeted_update(&raw);
assert!(should_wake_receiver(&mut tx, 42, &content).await?);
assert!(!should_wake_receiver(&mut tx, 43, &content).await?);
record_incoming_event(&ctx, &mut tx, 7, "included", &content).await?;
record_incoming_event(&ctx, &mut tx, 7, "included", &content).await?;
for raw in ["{}", "null", "not JSON", r#"{"someone-else":"Dinner"}"#] {
let content = targeted_update(raw);
assert!(!should_wake_receiver(&mut tx, 42, &content).await?);
record_incoming_event(&ctx, &mut tx, 7, "excluded", &content).await?;
}
let rows: Vec<(String, String)> =
sqlx::query_as("SELECT event_id, content FROM notification_outbox")
.fetch_all(&mut *tx)
.await?;
assert_eq!(
rows,
vec![("included".into(), "You share dinner: €20".into())]
);
// Older apps omit notify and retain their existing announcements.
assert!(should_wake_receiver(&mut tx, 43, &webxdc_update(Some("Your turn"))).await?);
Ok(())
}
fn pending_row(kind: &str) -> PendingRow { fn pending_row(kind: &str) -> PendingRow {
PendingRow { PendingRow {
event_id: "event".into(), event_id: "event".into(),

File diff suppressed because it is too large Load diff

View file

@ -134,7 +134,8 @@ impl WebxdcStore {
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
let now = chrono::Utc::now().timestamp(); let now = chrono::Utc::now().timestamp();
for entry in &catalog.entries { for (position, entry) in catalog.entries.iter().enumerate() {
let sort_order = position as i64;
if !Self::entry_is_sane(entry) { if !Self::entry_is_sane(entry) {
tracing::warn!(app_id = entry.app_id, "skipping unusable catalog entry"); tracing::warn!(app_id = entry.app_id, "skipping unusable catalog entry");
continue; continue;
@ -151,8 +152,8 @@ impl WebxdcStore {
r#"INSERT INTO webxdc_apps r#"INSERT INTO webxdc_apps
(app_id, version, name, name_translations, source_code_url, (app_id, version, name, name_translations, source_code_url,
description, icon, bundle_sha256, bundle_bytes, published, description, icon, bundle_sha256, bundle_bytes, published,
cached_at) cached_at, pro_only, sort_order, one_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
ON CONFLICT(app_id, version) DO UPDATE SET ON CONFLICT(app_id, version) DO UPDATE SET
name = excluded.name, name = excluded.name,
name_translations = excluded.name_translations, name_translations = excluded.name_translations,
@ -162,6 +163,9 @@ impl WebxdcStore {
bundle_sha256 = excluded.bundle_sha256, bundle_sha256 = excluded.bundle_sha256,
bundle_bytes = excluded.bundle_bytes, bundle_bytes = excluded.bundle_bytes,
published = 1, published = 1,
pro_only = excluded.pro_only,
sort_order = excluded.sort_order,
one_time = excluded.one_time,
cached_at = excluded.cached_at"#, cached_at = excluded.cached_at"#,
entry.app_id, entry.app_id,
entry.version, entry.version,
@ -173,6 +177,9 @@ impl WebxdcStore {
entry.bundle_sha256, entry.bundle_sha256,
entry.bundle_bytes, entry.bundle_bytes,
now, now,
entry.pro_only,
sort_order,
entry.one_time,
) )
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;

View file

@ -97,6 +97,10 @@
sendUpdateInterval: init.sendUpdateInterval, sendUpdateInterval: init.sendUpdateInterval,
sendUpdateMaxSize: init.sendUpdateMaxSize, sendUpdateMaxSize: init.sendUpdateMaxSize,
getMembers: function () {
return call('getMembers');
},
sendUpdate: function (update, descr) { sendUpdate: function (update, descr) {
return call('sendUpdate', { return call('sendUpdate', {
payload: update.payload === undefined ? null : update.payload, payload: update.payload === undefined ? null : update.payload,

View file

@ -44,6 +44,7 @@ UserConfig testUserConfig({
currentSignedPreKeyIndexStart: 100000, currentSignedPreKeyIndexStart: 100000,
hideChangeLog: true, hideChangeLog: true,
hideMemoriesBackupPromo: false, hideMemoriesBackupPromo: false,
hideWidgetShareExplainer: false,
updateFcmToken: true, updateFcmToken: true,
canUseLoginTokenForAuth: true, canUseLoginTokenForAuth: true,
isBackupEnabled: false, isBackupEnabled: false,

View file

@ -125,4 +125,32 @@ void main() {
expect(remainingIds.contains('msg_d_sender_deleted_expired'), isFalse); expect(remainingIds.contains('msg_d_sender_deleted_expired'), isFalse);
}, },
); );
test('deleting a chat retains only one-time app state', () async {
await twonlyDB.groupsDao.createNewGroup(
GroupsCompanion.insert(groupId: 'chat', groupName: 'Trip'),
);
for (final statement in [
"INSERT INTO messages(message_id, group_id, type) VALUES ('text', 'chat', 'text')",
"INSERT INTO messages(message_id, group_id, type) VALUES ('expenses-card', 'chat', 'webxdcApp')",
"INSERT INTO messages(message_id, group_id, type) VALUES ('game-card', 'chat', 'webxdcApp')",
"INSERT INTO webxdc_apps(app_id, version, name, bundle_sha256, bundle_bytes, cached_at, one_time) VALUES ('expenses', 1, 'Expenses', 'expense-hash', 1, 0, 1)",
"INSERT INTO webxdc_apps(app_id, version, name, bundle_sha256, bundle_bytes, cached_at, one_time) VALUES ('game', 1, 'Game', 'game-hash', 1, 0, 0)",
"INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token, created_at, last_update_at) VALUES ('expenses-card', 'chat', 'expenses', 1, 'expenses-origin', 0, 0)",
"INSERT INTO webxdc_instances(instance_id, group_id, app_id, version, origin_token, created_at, last_update_at) VALUES ('game-card', 'chat', 'game', 1, 'game-origin', 0, 0)",
"INSERT INTO webxdc_updates(instance_id, serial, message_id, payload, received_at) VALUES ('expenses-card', 1, 'expense-update', '{}', 0)",
]) {
await twonlyDB.customStatement(statement);
}
await twonlyDB.messagesDao.deleteMessagesByGroupId('chat');
final messages = await twonlyDB.select(twonlyDB.messages).get();
expect(messages.map((message) => message.messageId), ['expenses-card']);
final instances = await twonlyDB.select(twonlyDB.webxdcInstances).get();
expect(instances.map((instance) => instance.instanceId), ['expenses-card']);
final updates = await twonlyDB.select(twonlyDB.webxdcUpdates).get();
expect(updates, hasLength(1));
expect((await twonlyDB.groupsDao.getGroup('chat'))?.deletedContent, isTrue);
});
} }