Šepot — end-to-end encrypted messenger
Initial public release. Anonymous on-device identity (no phone number, e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double Ratchet, sealed sender) via libsignal, a metadata-minimizing relay, group chats, disappearing messages, encrypted local storage, optional Tor transport, and a UI localized into Czech, English, German, Russian, and Italian. Monorepo: Flutter client (app/), Rust relay + wire types (rust/), protobuf definitions (proto/), self-host deploy (deploy/). Licensed under AGPL-3.0 with an app-store distribution exception (§7).
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sepot/core/l10n/error_messages.dart';
|
||||
import 'package:sepot/core/l10n/l10n.dart';
|
||||
import 'package:sepot/core/theme/app_theme.dart';
|
||||
import 'package:sepot/providers/push_provider.dart';
|
||||
|
||||
/// Nastavení push notifikací — degooglovaný UnifiedPush.
|
||||
/// Zapnutí = výběr distributora (ntfy) + registrace endpointu u relaye.
|
||||
/// Budíček nenese obsah ani odesílatele; při probuzení se appka připojí
|
||||
/// a vyzvedne frontu.
|
||||
class PushSettingsTile extends ConsumerStatefulWidget {
|
||||
const PushSettingsTile({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<PushSettingsTile> createState() => _PushSettingsTileState();
|
||||
}
|
||||
|
||||
class _PushSettingsTileState extends ConsumerState<PushSettingsTile> {
|
||||
String _mode = 'off';
|
||||
String? _distributor;
|
||||
bool _busy = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refresh();
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final push = ref.read(pushServiceProvider);
|
||||
final mode = await push.currentMode();
|
||||
final d = await push.currentDistributor();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_mode = mode;
|
||||
_distributor = d;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _chooseMode() async {
|
||||
final picked = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => SimpleDialog(
|
||||
title: Text(ctx.l10n.pushTitle),
|
||||
children: [
|
||||
_modeOption(ctx, 'off', ctx.l10n.commonOff, ctx.l10n.pushModeOffDesc),
|
||||
_modeOption(ctx, 'up', ctx.l10n.pushModeUp, ctx.l10n.pushModeUpDesc),
|
||||
_modeOption(
|
||||
ctx, 'fcm', ctx.l10n.pushModeFcm, ctx.l10n.pushModeFcmDesc),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (picked == null || picked == _mode) return;
|
||||
|
||||
final push = ref.read(pushServiceProvider);
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
switch (picked) {
|
||||
case 'off':
|
||||
await push.setModeOff();
|
||||
case 'up':
|
||||
final ok = await push.setModeUnifiedPush();
|
||||
if (!ok && mounted) await _pickDistributor();
|
||||
case 'fcm':
|
||||
await push.setModeFcm();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n
|
||||
.pushSetupFailed(describeError(context.l10n, e)))),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await _refresh();
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _modeOption(
|
||||
BuildContext ctx, String value, String title, String subtitle) {
|
||||
return SimpleDialogOption(
|
||||
onPressed: () => Navigator.pop(ctx, value),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Icon(
|
||||
value == _mode
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_off,
|
||||
size: 18,
|
||||
color: value == _mode
|
||||
? SepotColors.ember
|
||||
: SepotColors.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: SepotColors.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickDistributor() async {
|
||||
final push = ref.read(pushServiceProvider);
|
||||
final list = await push.distributors();
|
||||
if (!mounted) return;
|
||||
|
||||
if (list.isEmpty) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(ctx.l10n.pushNoDistributorTitle),
|
||||
content: Text(ctx.l10n.pushNoDistributorBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(ctx.l10n.commonOk),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final chosen = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => SimpleDialog(
|
||||
title: Text(ctx.l10n.pushPickDistributorTitle),
|
||||
children: [
|
||||
for (final d in list)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => Navigator.pop(ctx, d),
|
||||
child: Text(d),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (chosen != null) {
|
||||
await push.useDistributor(chosen);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitle = switch (_mode) {
|
||||
'up' => context.l10n.pushSubtitleUnifiedPush(
|
||||
_distributor ?? context.l10n.pushNoDistributorFallback),
|
||||
'fcm' => context.l10n.pushSubtitleFcm,
|
||||
_ => context.l10n.commonOff,
|
||||
};
|
||||
return ListTile(
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: SepotColors.surfaceVariant,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications_outlined,
|
||||
size: 20,
|
||||
color: SepotColors.textSecondary,
|
||||
),
|
||||
),
|
||||
title: Text(context.l10n.pushTitle,
|
||||
style: const TextStyle(fontSize: 15)),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 13, color: SepotColors.textMuted),
|
||||
),
|
||||
trailing: _busy
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: SepotColors.ember,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.chevron_right, color: SepotColors.textMuted),
|
||||
onTap: _busy ? null : _chooseMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user