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

TF-3157 Implement web socket push #3168

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
}
29 changes: 28 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 @@ -44,14 +45,17 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oi
import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/bindings/email_rules_interactor_bindings.dart';
import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindings/forwarding_interactors_bindings.dart';
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart';
import 'package:tmail_ui_user/features/push_notification/domain/exceptions/web_socket_exceptions.dart';
import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_firebase_registration_state.dart';
import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_firebase_registration_state.dart';
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 +376,29 @@ 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) {
throw WebSocketPushNotSupportedException();
}
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 @@ -574,7 +574,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,90 @@

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/model/connect_web_socket_message.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/features/push_notification/domain/model/web_socket_action.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(ConnectWebSocketMessage(
webSocketAction: WebSocketAction.connect,
webSocketUrl: webSocketUri.toString(),
webSocketTicket: webSocketTicket
).toJson());

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;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:tmail_ui_user/features/push_notification/domain/model/web_socket_action.dart';

part 'connect_web_socket_message.g.dart';

@JsonSerializable()
class ConnectWebSocketMessage with EquatableMixin {
@JsonKey(name: 'action')
final WebSocketAction webSocketAction;
@JsonKey(name: 'url')
final String webSocketUrl;
@JsonKey(name: 'ticket')
final String webSocketTicket;

ConnectWebSocketMessage({
required this.webSocketAction,
required this.webSocketUrl,
required this.webSocketTicket,
});

factory ConnectWebSocketMessage.fromJson(Map<String, dynamic> json)
=> _$ConnectWebSocketMessageFromJson(json);
Map<String, dynamic> toJson() => _$ConnectWebSocketMessageToJson(this);

@override
List<Object?> get props => [webSocketAction, webSocketUrl, webSocketTicket];
}
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;
}
}
}
Loading
Loading