Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] TF-3157 Implement web socket push #3168

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
80d8cdd
TF-3157 Update jmap-dart-client dependency
tddang-linagora Sep 23, 2024
6833c03
TF-3157 Implement web socket push
tddang-linagora Sep 23, 2024
575edd9
fixup! TF-3157 Implement web socket push
tddang-linagora Sep 23, 2024
dbf30b0
fixup! TF-3157 Implement web socket push
tddang-linagora Sep 25, 2024
c5a0186
TF-3157 Update web socket with background service worker
tddang-linagora Sep 27, 2024
2eef328
fixup! TF-3157 Implement web socket push
tddang-linagora Oct 9, 2024
1fa0cdb
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 9, 2024
f3b6444
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 9, 2024
60eb5f2
TF-3157 Stub BroadcastChannel for mobile build
tddang-linagora Oct 9, 2024
27ba630
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 9, 2024
cb2f306
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 10, 2024
b664717
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 11, 2024
402b954
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 11, 2024
8d91f02
fixup! TF-3157 Update jmap-dart-client dependency
tddang-linagora Oct 14, 2024
3f18542
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 14, 2024
055b2c1
fixup! TF-3157 Implement web socket push
tddang-linagora Oct 22, 2024
e879dec
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 22, 2024
7775412
fixup! TF-3157 Implement web socket push
tddang-linagora Oct 23, 2024
196c3e8
fixup! TF-3157 Update web socket with background service worker
tddang-linagora Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions contact/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
ref: "enhancement/web-socket-ticket-capability"
resolved-ref: "06b1288f8e501c38166ec1895d227d4f832e4144"
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion contact/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
jmap_dart_client:
git:
url: https://github.com/linagora/jmap-dart-client.git
ref: main
ref: enhancement/web-socket-ticket-capability

### Dependencies from pub.dev ###
equatable: 2.0.5
Expand Down
1 change: 1 addition & 0 deletions core/lib/data/constants/constant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ class Constant {
static const octetStreamMimeType = 'application/octet-stream';
static const pdfExtension = '.pdf';
static const imageType = 'image';
static const wsServiceWorkerBroadcastChannel = 'background-message';
}
21 changes: 21 additions & 0 deletions docs/adr/0053-web-socket-data-synchronization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 53. Web socket data synchronization

Date: 2024-11-10

## Status

Accepted

## Context

- Currently Twake Mail web use Firebase Cloud Messaging to sync data on real time
- JMAP already implemented web socket push, which is more optimized for web

## Decision

- Web socket is implemented for real time update data for Twake Mail web
- Service worker is implemented for background tasks, helping web socket working in background

## Consequences

- Twake Mail web now no longer depends on Firebase Cloud Messaging, using web socket to update users' latest data
6 changes: 3 additions & 3 deletions email_recovery/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
ref: "enhancement/web-socket-ticket-capability"
resolved-ref: "06b1288f8e501c38166ec1895d227d4f832e4144"
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion email_recovery/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies:
jmap_dart_client:
git:
url: https://github.com/linagora/jmap-dart-client.git
ref: main
ref: enhancement/web-socket-ticket-capability

### Dependencies from pub.dev ###
equatable: 2.0.5
Expand Down
8 changes: 4 additions & 4 deletions fcm/lib/model/type_name.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import 'package:equatable/equatable.dart';

class TypeName with EquatableMixin {
static final mailboxType = TypeName('Mailbox');
static final emailType = TypeName('Email');
static final emailDelivery = TypeName('EmailDelivery');
static const mailboxType = TypeName('Mailbox');
static const emailType = TypeName('Email');
static const emailDelivery = TypeName('EmailDelivery');

final String value;

TypeName(this.value);
const TypeName(this.value);

@override
List<Object?> get props => [value];
Expand Down
6 changes: 3 additions & 3 deletions fcm/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
ref: "enhancement/web-socket-ticket-capability"
resolved-ref: "06b1288f8e501c38166ec1895d227d4f832e4144"
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion fcm/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies:
jmap_dart_client:
git:
url: https://github.com/linagora/jmap-dart-client.git
ref: main
ref: enhancement/web-socket-ticket-capability

### Dependencies from pub.dev ###
equatable: 2.0.5
Expand Down
6 changes: 3 additions & 3 deletions forward/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: f55a1862c1486197ea6a79feac31776ebeddc66c
ref: "enhancement/web-socket-ticket-capability"
resolved-ref: "06b1288f8e501c38166ec1895d227d4f832e4144"
url: "https://github.com/linagora/jmap-dart-client.git"
source: git
version: "0.2.2"
version: "0.2.3"
js:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion forward/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies:
jmap_dart_client:
git:
url: https://github.com/linagora/jmap-dart-client.git
ref: main
ref: enhancement/web-socket-ticket-capability

### Dependencies from pub.dev ###
equatable: 2.0.5
Expand Down
6 changes: 2 additions & 4 deletions lib/features/base/action/ui_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ abstract class Action with EquatableMixin {}

abstract class UIAction extends Action {}

abstract class FcmAction extends Action {}

abstract class FcmStateChangeAction extends FcmAction {
abstract class PushNotificationStateChangeAction extends Action {
final TypeName typeName;
final jmap.State newState;

FcmStateChangeAction(this.typeName, this.newState);
PushNotificationStateChangeAction(this.typeName, this.newState);
dab246 marked this conversation as resolved.
Show resolved Hide resolved
}
26 changes: 25 additions & 1 deletion lib/features/base/base_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import 'package:forward/forward/capability_forward.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/account/authentication_type.dart';
import 'package:model/model.dart';
import 'package:rule_filter/rule_filter/capability_rule_filter.dart';
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart';
Expand All @@ -49,9 +50,11 @@ import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored
import 'package:tmail_ui_user/features/push_notification/domain/usecases/destroy_firebase_registration_interactor.dart';
import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_firebase_registration_interactor.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/bindings/web_socket_interactor_bindings.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/config/fcm_configuration.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_token_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/controller/web_socket_controller.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart';
Expand Down Expand Up @@ -372,6 +375,27 @@ abstract class BaseController extends GetxController
}
}

void injectWebSocket(Session? session, AccountId? accountId) {
try {
requireCapability(
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
session!,
accountId!,
[
CapabilityIdentifier.jmapWebSocket,
CapabilityIdentifier.jmapWebSocketTicket
]
);
final wsCapability = session.getCapabilityProperties<WebSocketCapability>(
accountId,
CapabilityIdentifier.jmapWebSocket);
if (wsCapability?.supportsPush != true) return;
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
WebSocketInteractorBindings().dependencies();
WebSocketController.instance.initialize(accountId: accountId, session: session);
} catch(e) {
logError('$runtimeType::injectWebSocket(): exception: $e');
}
}

AuthenticationType get authenticationType => authorizationInterceptors.authenticationType;

bool get isAuthenticatedWithOidc => authenticationType == AuthenticationType.oidc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,11 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
injectAutoCompleteBindings(session, currentAccountId);
injectRuleFilterBindings(session, currentAccountId);
injectVacationBindings(session, currentAccountId);
injectFCMBindings(session, currentAccountId);
if (PlatformInfo.isWeb) {
injectWebSocket(session, currentAccountId);
} else {
injectFCMBindings(session, currentAccountId);
}

_getVacationResponse();
spamReportController.getSpamReportStateAction();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';

abstract class WebSocketDatasource {
Stream<dynamic> getWebSocketChannel(Session session, AccountId accountId);
dab246 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@

import 'package:core/data/constants/constant.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/broadcast_channel/broadcast_channel.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:rxdart/transformers.dart';
import 'package:tmail_ui_user/features/push_notification/data/datasource/web_socket_datasource.dart';
import 'package:tmail_ui_user/features/push_notification/data/network/web_socket_api.dart';
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
import 'package:universal_html/html.dart';

class WebSocketDatasourceImpl implements WebSocketDatasource {
final WebSocketApi _webSocketApi;
final ExceptionThrower _exceptionThrower;

const WebSocketDatasourceImpl(this._webSocketApi, this._exceptionThrower);

static const String _webSocketClosed = 'webSocketClosed';

@override
Stream getWebSocketChannel(Session session, AccountId accountId) {
return Stream
.castFrom(_getWebSocketChannel(session, accountId))
.doOnError(_exceptionThrower.throwException);
}

Stream _getWebSocketChannel(
Session session,
AccountId accountId,
) async* {
final broadcastChannel = BroadcastChannel(Constant.wsServiceWorkerBroadcastChannel);
try {
_verifyWebSocketCapabilities(session, accountId);
final webSocketTicket = await _webSocketApi.getWebSocketTicket(session, accountId);
final webSocketUri = _getWebSocketUri(session, accountId);
window.navigator.serviceWorker?.controller?.postMessage({
'action': 'connect',
'url': webSocketUri.toString(),
'ticket': webSocketTicket,
});

yield* _webSocketListener(broadcastChannel);
} catch (e) {
logError('RemoteWebSocketDatasourceImpl::getWebSocketChannel():error: $e');
rethrow;
}
}

void _verifyWebSocketCapabilities(Session session, AccountId accountId) {
if (!CapabilityIdentifier.jmapWebSocket.isSupported(session, accountId)
|| !CapabilityIdentifier.jmapWebSocketTicket.isSupported(session, accountId)
|| session.getCapabilityProperties<WebSocketCapability>(
accountId,
CapabilityIdentifier.jmapWebSocket)?.supportsPush != true
) {
throw WebSocketPushNotSupportedException();
}
}

Uri _getWebSocketUri(Session session, AccountId accountId) {
final webSocketCapability = session.getCapabilityProperties<WebSocketCapability>(
accountId,
CapabilityIdentifier.jmapWebSocket);
if (webSocketCapability?.supportsPush != true) {
throw WebSocketPushNotSupportedException();
}
final webSocketUri = webSocketCapability?.url;
if (webSocketUri == null) throw WebSocketUriUnavailableException();

return webSocketUri;
}

Stream _webSocketListener(BroadcastChannel broadcastChannel) {
return broadcastChannel.onMessage.map((event) {
if (event.data == _webSocketClosed) {
throw WebSocketClosedException();
}

return event.data;
});
}
}
31 changes: 31 additions & 0 deletions lib/features/push_notification/data/model/web_socket_echo.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:json_annotation/json_annotation.dart';

part 'web_socket_echo.g.dart';

@JsonSerializable(includeIfNull: false)
class WebSocketEcho {
@JsonKey(name: '@type')
final String? type;
final String? requestId;
final List<List<dynamic>>? methodResponses;

WebSocketEcho({
this.type,
this.requestId,
this.methodResponses,
});

factory WebSocketEcho.fromJson(Map<String, dynamic> json) => _$WebSocketEchoFromJson(json);

Map<String, dynamic> toJson() => _$WebSocketEchoToJson(this);

static bool isValid(Map<String, dynamic> json) {
try {
final webSocketEcho = WebSocketEcho.fromJson(json);
final listResponses = webSocketEcho.methodResponses?.firstOrNull;
return listResponses?.contains('Core/echo') ?? false;
} catch (_) {
return false;
}
}
}
33 changes: 33 additions & 0 deletions lib/features/push_notification/data/model/web_socket_ticket.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'web_socket_ticket.g.dart';

@JsonSerializable(includeIfNull: false)
class WebSocketTicket with EquatableMixin {
final String? value;
final String? clientAddress;
final DateTime? generatedOn;
final DateTime? validUntil;
final String? username;

WebSocketTicket({
required this.value,
required this.clientAddress,
required this.generatedOn,
required this.validUntil,
required this.username,
});

factory WebSocketTicket.fromJson(Map<String, dynamic> json) => _$WebSocketTicketFromJson(json);
Map<String, dynamic> toJson() => _$WebSocketTicketToJson(this);

@override
List<Object?> get props => [
value,
clientAddress,
generatedOn,
validUntil,
username,
];
}
Loading
Loading