Skip to content
Merged
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
1 change: 1 addition & 0 deletions lib/extensions/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export 'request_failure.dart';
export 'route_match.dart';
export 'rtc_peer_connection_extension.dart';
export 'secure_storage_extension.dart';
export 'session_issue.dart';
export 'session_status.dart';
export 'signaling_exception.dart';
export 'signaling_hangup_failure.dart';
Expand Down
38 changes: 38 additions & 0 deletions lib/extensions/session_issue.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';

import 'package:webtrit_phone/l10n/l10n.dart';
import 'package:webtrit_phone/models/models.dart';

extension SessionIssueSeverityColor on SessionIssueSeverity {
// TODO(Serdun): Move to color scheme
Color color(BuildContext context) {
switch (this) {
case SessionIssueSeverity.critical:
return Colors.red;
case SessionIssueSeverity.warning:
return Colors.orange;
case SessionIssueSeverity.info:
return Colors.blueGrey;
}
}
}
Comment on lines +6 to +18

extension SessionIssueL10n on SessionIssue {
/// Short title shown where a single line is available (e.g. account row title).
String title(BuildContext context) {
switch (id) {
case SessionIssueId.limitedStandaloneCallMode:
return context.l10n.sessionStatus_issue_limitedStandaloneCallMode_title;
}
}

/// One-line caption summarizing the issue (e.g. account row subtitle).
String caption(BuildContext context) {
switch (id) {
case SessionIssueId.limitedStandaloneCallMode:
return context.l10n.sessionStatus_issue_limitedStandaloneCallMode_caption;
}
}

Color color(BuildContext context) => severity.color(context);
}
28 changes: 28 additions & 0 deletions lib/features/session_status/bloc/session_status_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:logging/logging.dart';

import 'package:webtrit_callkeep/webtrit_callkeep.dart';

import 'package:webtrit_phone/features/features.dart';
import 'package:webtrit_phone/models/models.dart';

Expand All @@ -22,13 +24,15 @@ class SessionStatusCubit extends Cubit<SessionStatusState> {
_callSubscription = callBloc.stream.listen(_onCallChanged);

_emitCombinedStatus();
_fetchCallDeliveryMode();
}

late final StreamSubscription<PushTokensState> _pushTokensSubscription;
late final StreamSubscription<CallState> _callSubscription;

PushTokensState? _lastPushTokensState;
CallState? _lastCallState;
CallkeepAndroidCallDeliveryMode _callDeliveryMode = CallkeepAndroidCallDeliveryMode.unknown;

void _onPushTokensChanged(PushTokensState pushTokens) {
_lastPushTokensState = pushTokens;
Expand All @@ -40,6 +44,29 @@ class SessionStatusCubit extends Cubit<SessionStatusState> {
_emitCombinedStatus();
}

/// The call-delivery mode is a device capability that does not change at
/// runtime, so it is fetched once at session start.
Future<void> _fetchCallDeliveryMode() async {
try {
_callDeliveryMode = await WebtritCallkeepPermissions().getCallDeliveryMode();
_emitCombinedStatus();
} catch (e) {
_logger.warning('fetchCallDeliveryMode', e);
}
}

/// Builds the generic side-issue list from the known sources. Add a source
/// here to surface a new issue; the UI consumes the list generically.
List<SessionIssue> _buildIssues() {
final issues = <SessionIssue>[];
if (_callDeliveryMode == CallkeepAndroidCallDeliveryMode.standalone) {
issues.add(
const SessionIssue(id: SessionIssueId.limitedStandaloneCallMode, severity: SessionIssueSeverity.warning),
);
}
return issues;
}

void _emitCombinedStatus() {
_logger.finest('emitCombinedStatus: $_lastPushTokensState, $_lastCallState');

Expand All @@ -56,6 +83,7 @@ class SessionStatusCubit extends Cubit<SessionStatusState> {
emit(
state.copyWith(
status: SessionStatus(signalingStatus: call.status, pushTokenError: pushTokenError),
issues: _buildIssues(),
),
);
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion lib/features/session_status/bloc/session_status_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@ part of 'session_status_cubit.dart';

@freezed
class SessionStatusState with _$SessionStatusState {
const SessionStatusState({this.status = const SessionStatus(signalingStatus: CallStatus.inProgress)});
const SessionStatusState({
this.status = const SessionStatus(signalingStatus: CallStatus.inProgress),
this.issues = const [],
});

@override
final SessionStatus status;

/// Side issues independent of the primary signaling [status] (e.g. limited
/// standalone call mode). Drives the avatar indicator and account summary.
@override
final List<SessionIssue> issues;

bool get hasIssues => issues.isNotEmpty;

/// The most severe issue (ties resolved by first occurrence), or null.
SessionIssue? get topIssue =>
issues.isEmpty ? null : issues.reduce((a, b) => b.severity.index > a.severity.index ? b : a);
}
1 change: 1 addition & 0 deletions lib/features/session_status/session_status.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'bloc/session_status_cubit.dart';
export 'view/view.dart';
export 'widgets/widgets.dart';
24 changes: 24 additions & 0 deletions lib/features/session_status/widgets/session_issue_badge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';

/// Small circular "!" badge used to flag that the session has at least one side
/// issue. Color is driven by the issue severity; callers position it as an
/// overlay (e.g. on the user avatar).
class SessionIssueBadge extends StatelessWidget {
const SessionIssueBadge({super.key, required this.color, this.size = 14});

final Color color;
final double size;

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Theme.of(context).colorScheme.surface, width: 1.5),
),
child: Icon(Icons.priority_high, color: Colors.white, size: size),
Comment thread
SERDUN marked this conversation as resolved.
);
}
}
1 change: 1 addition & 0 deletions lib/features/session_status/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'session_issue_badge.dart';
15 changes: 11 additions & 4 deletions lib/features/settings/view/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,19 @@ class SettingsScreen extends StatelessWidget {
),
children: [
BlocBuilder<UserInfoCubit, UserInfoState>(
builder: (context, state) => UserInfoListTile(info: state.userInfo),
builder: (context, state) => UserInfoListTile(
info: state.userInfo,
topIssue: context.watch<SessionStatusCubit>().state.topIssue,
),
),
BlocBuilder<SessionStatusCubit, SessionStatusState>(
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, sessionState) =>
SessionStatusListTile(status: sessionState.status, onTap: () => _onDiagnosticTap(context)),
buildWhen: (previous, current) =>
previous.status != current.status || previous.topIssue != current.topIssue,
builder: (context, sessionState) => SessionStatusListTile(
status: sessionState.status,
topIssue: sessionState.topIssue,
onTap: () => _onDiagnosticTap(context),
),
),
if (showSeparators) ListTileSeparator(color: effectiveStyle?.separatorColor),
BlocBuilder<RegisterStatusCubit, RegisterStatus>(
Expand Down
18 changes: 17 additions & 1 deletion lib/features/settings/widgets/session_status_list_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@ import '../../microphone_status/microphone_status.dart';
import '../settings.dart';

class SessionStatusListTile extends StatelessWidget {
const SessionStatusListTile({super.key, required this.status, this.info, this.onTap, this.contentPadding});
const SessionStatusListTile({
super.key,
required this.status,
this.topIssue,
this.info,
this.onTap,
this.contentPadding,
});

final SessionStatus status;

/// Most severe active side issue, surfaced as a warning subtitle. Null hides it.
final SessionIssue? topIssue;
final UserInfo? info;
final VoidCallback? onTap;
final EdgeInsetsGeometry? contentPadding;
Expand All @@ -35,6 +45,12 @@ class SessionStatusListTile extends StatelessWidget {
child: CircleAvatar(radius: 4, backgroundColor: status.color(context)),
),
title: Text(status.l10n(context), key: status.key, style: themeData.textTheme.labelLarge),
subtitle: topIssue == null
? null
: Text(
topIssue!.caption(context),
style: themeData.textTheme.bodySmall?.copyWith(color: topIssue!.color(context)),
),
trailing: const Icon(Icons.arrow_right),
),
BlocBuilder<MicrophoneStatusBloc, MicrophoneStatusState>(
Expand Down
25 changes: 18 additions & 7 deletions lib/features/settings/widgets/user_info_list_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import 'package:flutter/material.dart';
import 'package:webtrit_phone/models/models.dart';

import 'package:webtrit_phone/extensions/extensions.dart';
import 'package:webtrit_phone/features/session_status/session_status.dart';
import 'package:webtrit_phone/utils/utils.dart';
Comment on lines 3 to 7
import 'package:webtrit_phone/widgets/widgets.dart';

class UserInfoListTile extends StatelessWidget {
const UserInfoListTile({super.key, this.info, this.onEditPressed, this.contentPadding});
const UserInfoListTile({super.key, this.info, this.topIssue, this.onEditPressed, this.contentPadding});

final UserInfo? info;

/// Most severe active side issue, shown as a badge overlay on the avatar. Null hides it.
final SessionIssue? topIssue;
final VoidCallback? onEditPressed;

final EdgeInsetsGeometry? contentPadding;
Expand Down Expand Up @@ -37,12 +41,19 @@ class UserInfoListTile extends StatelessWidget {
minimum: resolvedContentPadding,
child: Row(
children: [
LeadingAvatar(
username: info?.name ?? info?.numbers.main,
thumbnailUrl: gravatarThumbnailUrl(info?.email),
radius: radius,
showLoading: true,
loadingPadding: EdgeInsets.zero,
Stack(
clipBehavior: Clip.none,
children: [
LeadingAvatar(
username: info?.name ?? info?.numbers.main,
thumbnailUrl: gravatarThumbnailUrl(info?.email),
radius: radius,
showLoading: true,
loadingPadding: EdgeInsets.zero,
),
if (topIssue != null)
Positioned(right: -2, bottom: -2, child: SessionIssueBadge(color: topIssue!.color(context))),
],
),
const SizedBox(width: 8),
Expanded(
Expand Down
12 changes: 12 additions & 0 deletions lib/l10n/app_localizations.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3312,6 +3312,18 @@ abstract class AppLocalizations {
/// **'Problem with configuration push notification service'**
String get sessionStatus_pushNotificationServiceProblem;

/// No description provided for @sessionStatus_issue_limitedStandaloneCallMode_title.
///
/// In en, this message translates to:
/// **'Limited call mode'**
String get sessionStatus_issue_limitedStandaloneCallMode_title;

/// No description provided for @sessionStatus_issue_limitedStandaloneCallMode_caption.
///
/// In en, this message translates to:
/// **'Standalone - delivery may be delayed'**
String get sessionStatus_issue_limitedStandaloneCallMode_caption;

/// Status message displayed while the application is performing cleanup during the logout process.
///
/// In en, this message translates to:
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_localizations.g.mapper.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lib/l10n/app_localizations_en.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sessionStatus_pushNotificationServiceProblem => 'Problem with configuration push notification service';

@override
String get sessionStatus_issue_limitedStandaloneCallMode_title => 'Limited call mode';

@override
String get sessionStatus_issue_limitedStandaloneCallMode_caption => 'Standalone - delivery may be delayed';

@override
String get session_Teardown_progressText => 'Signing out...';

Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations_it.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,12 @@ class AppLocalizationsIt extends AppLocalizations {
String get sessionStatus_pushNotificationServiceProblem =>
'Problema con la configurazione del servizio di notifiche push';

@override
String get sessionStatus_issue_limitedStandaloneCallMode_title => 'Modalità chiamate limitata';

@override
String get sessionStatus_issue_limitedStandaloneCallMode_caption => 'Standalone - possibile ritardo nella consegna';

@override
String get session_Teardown_progressText => 'Disconnessione in corso...';

Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations_uk.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get sessionStatus_pushNotificationServiceProblem => 'Проблема з налаштуванням служби пуш-сповіщень';

@override
String get sessionStatus_issue_limitedStandaloneCallMode_title => 'Обмежений режим дзвінків';

@override
String get sessionStatus_issue_limitedStandaloneCallMode_caption => 'Standalone — можлива затримка доставки';

@override
String get session_Teardown_progressText => 'Вихід із системи...';

Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,10 @@
},
"sessionStatus_pushNotificationServiceProblem": "Problem with configuration push notification service",
"@sessionStatus_pushNotificationServiceProblem": {},
"sessionStatus_issue_limitedStandaloneCallMode_title": "Limited call mode",
"@sessionStatus_issue_limitedStandaloneCallMode_title": {},
"sessionStatus_issue_limitedStandaloneCallMode_caption": "Standalone - delivery may be delayed",
"@sessionStatus_issue_limitedStandaloneCallMode_caption": {},
"session_Teardown_progressText": "Signing out...",
"@session_Teardown_progressText": {
"description": "Status message displayed while the application is performing cleanup during the logout process."
Expand Down
Loading
Loading