mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 08:32:13 +00:00
Improved: Onboarding flow for new users.
This commit is contained in:
parent
c0e45cfe1f
commit
2d6a2e436f
15 changed files with 660 additions and 550 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
## 0.2.17
|
## 0.2.17
|
||||||
|
|
||||||
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
||||||
|
- Improved: Onboarding flow for new users.
|
||||||
- Improved: The blue verification checkmark now displays the total number of verifications.
|
- Improved: The blue verification checkmark now displays the total number of verifications.
|
||||||
- Fix: Issue with receiving messages when user closed app while decrypting
|
- Fix: Issue with receiving messages when user closed app while decrypting
|
||||||
- Fix: Background message fetching reliability.
|
- Fix: Background message fetching reliability.
|
||||||
|
|
|
||||||
13
lib/app.dart
13
lib/app.dart
|
|
@ -137,12 +137,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
bool _isTwonlyLocked = true;
|
bool _isTwonlyLocked = true;
|
||||||
bool _wasLogged = true;
|
bool _wasLogged = true;
|
||||||
|
late int _initialPage;
|
||||||
|
|
||||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_initialPage = widget.initialPage;
|
||||||
Log.info('AppWidgetState: initState started');
|
Log.info('AppWidgetState: initState started');
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +152,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
Log.info('AppWidgetState: initAsync started');
|
Log.info('AppWidgetState: initAsync started');
|
||||||
if (userService.isUserCreated) {
|
if (userService.isUserCreated) {
|
||||||
|
if (_initialPage != 0) {
|
||||||
|
final count = await twonlyDB.contactsDao.getContactsCount();
|
||||||
|
if (count == 0) {
|
||||||
|
_initialPage = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
unawaited(FirebaseMessaging.instance.requestPermission());
|
unawaited(FirebaseMessaging.instance.requestPermission());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -200,8 +208,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
_isTwonlyLocked = false;
|
_isTwonlyLocked = false;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (!userService.currentUser.skipSetupPages &&
|
} else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
|
||||||
userService.currentUser.currentSetupPage != null) {
|
|
||||||
// This will only be shown in case the user have not skipped
|
// This will only be shown in case the user have not skipped
|
||||||
child = SetupView(
|
child = SetupView(
|
||||||
onUpdate: () => setState(() {
|
onUpdate: () => setState(() {
|
||||||
|
|
@ -210,7 +217,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = HomeView(
|
child = HomeView(
|
||||||
initialPage: widget.initialPage,
|
initialPage: _initialPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (_showOnboarding) {
|
} else if (_showOnboarding) {
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
return select(contacts).get();
|
return select(contacts).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getContactsCount() async {
|
||||||
|
final count = contacts.userId.count();
|
||||||
|
final query = selectOnly(contacts)..addColumns([count]);
|
||||||
|
final result = await query.map((row) => row.read(count)).getSingle();
|
||||||
|
return result ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
Stream<int?> watchContactsBlocked() {
|
Stream<int?> watchContactsBlocked() {
|
||||||
final count = contacts.userId.count();
|
final count = contacts.userId.count();
|
||||||
final query = selectOnly(contacts)
|
final query = selectOnly(contacts)
|
||||||
|
|
|
||||||
56
lib/src/visual/components/contact_request_badge.comp.dart
Normal file
56
lib/src/visual/components/contact_request_badge.comp.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
class ContactRequestBadgeComp extends StatelessWidget {
|
||||||
|
const ContactRequestBadgeComp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<int?>(
|
||||||
|
stream: twonlyDB.contactsDao.watchContactsRequestedCount(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final count = snapshot.data ?? 0;
|
||||||
|
if (count == 0) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: primaryColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: NotificationBadgeComp(
|
||||||
|
backgroundColor: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
|
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
count: count.toString(),
|
||||||
|
child: IconButton(
|
||||||
|
color: Colors.black,
|
||||||
|
icon: const FaIcon(
|
||||||
|
FontAwesomeIcons.userPlus,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,53 +44,52 @@ class _ProfileQrCodeCompState extends State<ProfileQrCodeComp> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading || _qrCode == null) {
|
final loaded = !_isLoading && _qrCode != null;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: widget.size,
|
width: widget.size,
|
||||||
height: widget.size,
|
height: widget.size,
|
||||||
child: const Center(
|
child: AnimatedSwitcher(
|
||||||
child: CircularProgressIndicator(),
|
duration: const Duration(milliseconds: 150),
|
||||||
),
|
child: loaded
|
||||||
);
|
? Container(
|
||||||
}
|
key: const ValueKey('qr_code_container'),
|
||||||
|
// padding: const EdgeInsets.all(3),
|
||||||
return Container(
|
decoration: BoxDecoration(
|
||||||
// padding: const EdgeInsets.all(3),
|
color: context.color.primary,
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: context.color.primary,
|
boxShadow: const [
|
||||||
borderRadius: BorderRadius.circular(12),
|
BoxShadow(
|
||||||
boxShadow: const [
|
color: Colors.black26,
|
||||||
BoxShadow(
|
blurRadius: 3,
|
||||||
color: Colors.black26,
|
offset: Offset(0, 2),
|
||||||
blurRadius: 3,
|
),
|
||||||
offset: Offset(0, 2),
|
],
|
||||||
),
|
),
|
||||||
],
|
child: QrImageView.withQr(
|
||||||
),
|
qr: QrCode.fromData(
|
||||||
child: QrImageView.withQr(
|
data: _qrCode!,
|
||||||
qr: QrCode.fromData(
|
errorCorrectLevel: QrErrorCorrectLevel.M,
|
||||||
data: _qrCode!,
|
),
|
||||||
errorCorrectLevel: QrErrorCorrectLevel.M,
|
eyeStyle: QrEyeStyle(
|
||||||
),
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
eyeStyle: QrEyeStyle(
|
borderRadius: 2,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
),
|
||||||
borderRadius: 2,
|
dataModuleStyle: QrDataModuleStyle(
|
||||||
),
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
dataModuleStyle: QrDataModuleStyle(
|
borderRadius: 2,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
),
|
||||||
borderRadius: 2,
|
gapless: false,
|
||||||
),
|
embeddedImage: (widget.showAvatar && _userAvatar != null) ? MemoryImage(_userAvatar!) : null,
|
||||||
gapless: false,
|
embeddedImageStyle: QrEmbeddedImageStyle(
|
||||||
embeddedImage: (widget.showAvatar && _userAvatar != null)
|
size: const Size(60, 66),
|
||||||
? MemoryImage(_userAvatar!)
|
embeddedImageShape: EmbeddedImageShape.square,
|
||||||
: null,
|
shapeColor: context.color.primary,
|
||||||
embeddedImageStyle: QrEmbeddedImageStyle(
|
safeArea: true,
|
||||||
size: const Size(60, 66),
|
),
|
||||||
embeddedImageShape: EmbeddedImageShape.square,
|
size: widget.size,
|
||||||
shapeColor: context.color.primary,
|
),
|
||||||
safeArea: true,
|
)
|
||||||
),
|
: const SizedBox.shrink(key: ValueKey('qr_code_placeholder')),
|
||||||
size: widget.size,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ import 'dart:collection';
|
||||||
|
|
||||||
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:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -15,6 +13,7 @@ import 'package:twonly/src/services/flame.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/components/contact_request_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||||
import 'package:twonly/src/visual/elements/headline.element.dart';
|
import 'package:twonly/src/visual/elements/headline.element.dart';
|
||||||
|
|
@ -22,6 +21,7 @@ import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
|
||||||
|
|
||||||
class ShareImageView extends StatefulWidget {
|
class ShareImageView extends StatefulWidget {
|
||||||
const ShareImageView({
|
const ShareImageView({
|
||||||
|
|
@ -111,9 +111,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
|
|
||||||
for (final group in groups) {
|
for (final group in groups) {
|
||||||
if (group.pinned) continue;
|
if (group.pinned) continue;
|
||||||
if (!group.archived &&
|
if (!group.archived && getFlameCounterFromGroup(group).counter > 0 && bestFriends.length < 6) {
|
||||||
getFlameCounterFromGroup(group).counter > 0 &&
|
|
||||||
bestFriends.length < 6) {
|
|
||||||
bestFriends.add(group);
|
bestFriends.add(group);
|
||||||
} else {
|
} else {
|
||||||
otherUsers.add(group);
|
otherUsers.add(group);
|
||||||
|
|
@ -133,10 +131,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
await updateGroups(
|
await updateGroups(
|
||||||
_allGroups
|
_allGroups
|
||||||
.where(
|
.where(
|
||||||
(x) =>
|
(x) => !x.archived || !hideArchivedUsers || widget.selectedGroupIds.contains(x.groupId),
|
||||||
!x.archived ||
|
|
||||||
!hideArchivedUsers ||
|
|
||||||
widget.selectedGroupIds.contains(x.groupId),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
@ -160,31 +155,23 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.lang.shareImageTitle),
|
title: Text(context.lang.shareImageTitle),
|
||||||
|
actions: const [
|
||||||
|
ContactRequestBadgeComp(),
|
||||||
|
SizedBox(width: 15),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: 40,
|
bottom: 40,
|
||||||
left: 10,
|
left: 10,
|
||||||
top: 20,
|
|
||||||
right: 10,
|
right: 10,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (_allGroups.isEmpty)
|
if (_allGroups.isEmpty)
|
||||||
Expanded(
|
const EmptyChatListComp()
|
||||||
child: Center(
|
else ...[
|
||||||
child: FilledButton.icon(
|
|
||||||
icon: const Icon(Icons.person_add),
|
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
|
||||||
label: Text(
|
|
||||||
context.lang.chatListViewSearchUserNameBtn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (_allGroups.isNotEmpty)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
|
@ -195,163 +182,161 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ShortcutRowComp(
|
ShortcutRowComp(
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
),
|
),
|
||||||
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
|
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
|
||||||
BestFriendsSelector(
|
BestFriendsSelector(
|
||||||
groups: _pinnedContacts,
|
groups: _pinnedContacts,
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
title: context.lang.shareImagePinnedContacts,
|
title: context.lang.shareImagePinnedContacts,
|
||||||
showSelectAll:
|
showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
BestFriendsSelector(
|
||||||
BestFriendsSelector(
|
groups: _bestFriends,
|
||||||
groups: _bestFriends,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
title: context.lang.shareImageBestFriends,
|
||||||
title: context.lang.shareImageBestFriends,
|
showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||||
showSelectAll:
|
),
|
||||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
const SizedBox(height: 10),
|
||||||
),
|
if (_otherUsers.isNotEmpty)
|
||||||
const SizedBox(height: 10),
|
Row(
|
||||||
if (_otherUsers.isNotEmpty)
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
HeadLineComp(context.lang.shareImageAllUsers),
|
||||||
children: [
|
if (_allGroups.any((x) => x.archived))
|
||||||
HeadLineComp(context.lang.shareImageAllUsers),
|
Row(
|
||||||
if (_allGroups.any((x) => x.archived))
|
children: [
|
||||||
Row(
|
Text(
|
||||||
children: [
|
context.lang.shareImageShowArchived,
|
||||||
Text(
|
style: const TextStyle(fontSize: 10),
|
||||||
context.lang.shareImageShowArchived,
|
),
|
||||||
style: const TextStyle(fontSize: 10),
|
Transform.scale(
|
||||||
),
|
scale: 0.75,
|
||||||
Transform.scale(
|
child: Checkbox(
|
||||||
scale: 0.75,
|
value: !hideArchivedUsers,
|
||||||
child: Checkbox(
|
side: WidgetStateBorderSide.resolveWith(
|
||||||
value: !hideArchivedUsers,
|
(states) {
|
||||||
side: WidgetStateBorderSide.resolveWith(
|
if (states.contains(WidgetState.selected)) {
|
||||||
(states) {
|
return const BorderSide(width: 0);
|
||||||
if (states.contains(WidgetState.selected)) {
|
}
|
||||||
return const BorderSide(width: 0);
|
return BorderSide(
|
||||||
}
|
color: Theme.of(
|
||||||
return BorderSide(
|
context,
|
||||||
color: Theme.of(
|
).colorScheme.outline,
|
||||||
context,
|
);
|
||||||
).colorScheme.outline,
|
},
|
||||||
);
|
),
|
||||||
|
onChanged: (a) async {
|
||||||
|
hideArchivedUsers = !hideArchivedUsers;
|
||||||
|
await _filterUsers(lastQuery);
|
||||||
|
if (mounted) setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onChanged: (a) async {
|
|
||||||
hideArchivedUsers = !hideArchivedUsers;
|
|
||||||
await _filterUsers(lastQuery);
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
if (_otherUsers.isNotEmpty)
|
||||||
if (_otherUsers.isNotEmpty)
|
UserList(
|
||||||
Expanded(
|
|
||||||
child: UserList(
|
|
||||||
List.from(_otherUsers),
|
List.from(_otherUsers),
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: SizedBox(
|
floatingActionButton: _allGroups.isEmpty
|
||||||
height: 168,
|
? null
|
||||||
child: Padding(
|
: SizedBox(
|
||||||
padding: const EdgeInsets.only(bottom: 20, right: 20),
|
height: 168,
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
padding: const EdgeInsets.only(bottom: 20, right: 20),
|
||||||
children: [
|
child: Column(
|
||||||
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
_screenshotImage?.image != null &&
|
children: [
|
||||||
userService.currentUser.showShowImagePreviewWhenSending)
|
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
|
||||||
SizedBox(
|
_screenshotImage?.image != null &&
|
||||||
height: 100,
|
userService.currentUser.showShowImagePreviewWhenSending)
|
||||||
width: 100 * 9 / 16,
|
SizedBox(
|
||||||
child: Container(
|
height: 100,
|
||||||
clipBehavior: Clip.hardEdge,
|
width: 100 * 9 / 16,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
border: Border.all(
|
clipBehavior: Clip.hardEdge,
|
||||||
color: context.color.primary,
|
decoration: BoxDecoration(
|
||||||
width: 2,
|
border: Border.all(
|
||||||
),
|
color: context.color.primary,
|
||||||
color: context.color.primary,
|
width: 2,
|
||||||
borderRadius: BorderRadius.circular(12),
|
),
|
||||||
),
|
color: context.color.primary,
|
||||||
child: ClipRRect(
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderRadius: BorderRadius.circular(12),
|
),
|
||||||
child: CustomPaint(
|
child: ClipRRect(
|
||||||
painter: UiImagePainter(_screenshotImage!.image!),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
child: CustomPaint(
|
||||||
),
|
painter: UiImagePainter(_screenshotImage!.image!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
|
||||||
icon: !mediaStoreFutureReady || sendingImage
|
|
||||||
? SizedBox(
|
|
||||||
height: 12,
|
|
||||||
width: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Theme.of(context).colorScheme.inversePrimary,
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
FilledButton.icon(
|
||||||
onPressed: () async {
|
icon: !mediaStoreFutureReady || sendingImage
|
||||||
if (!mediaStoreFutureReady ||
|
? SizedBox(
|
||||||
widget.selectedGroupIds.isEmpty) {
|
height: 12,
|
||||||
return;
|
width: 12,
|
||||||
}
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||||
|
onPressed: () async {
|
||||||
|
if (!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
sendingImage = true;
|
sendingImage = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
|
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
|
||||||
await insertMediaFileInMessagesTable(
|
await insertMediaFileInMessagesTable(
|
||||||
widget.mediaFileService,
|
widget.mediaFileService,
|
||||||
widget.selectedGroupIds.toList(),
|
widget.selectedGroupIds.toList(),
|
||||||
additionalData: widget.additionalData,
|
additionalData: widget.additionalData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
),
|
),
|
||||||
backgroundColor: WidgetStateProperty.all<Color>(
|
backgroundColor: WidgetStateProperty.all<Color>(
|
||||||
!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
|
!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
|
||||||
? context.color.onSurface
|
? context.color.onSurface
|
||||||
: context.color.primary,
|
: context.color.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,6 +360,8 @@ class UserList extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
restorationId: 'new_message_users_list',
|
restorationId: 'new_message_users_list',
|
||||||
itemCount: groups.length,
|
itemCount: groups.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/connection_status.comp.dart';
|
import 'package:twonly/src/visual/components/connection_status.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
|
||||||
|
|
@ -31,11 +32,15 @@ class ChatListView extends StatefulWidget {
|
||||||
|
|
||||||
class _ChatListViewState extends State<ChatListView> {
|
class _ChatListViewState extends State<ChatListView> {
|
||||||
StreamSubscription<void>? _userSub;
|
StreamSubscription<void>? _userSub;
|
||||||
late StreamSubscription<List<Group>> _contactsSub;
|
StreamSubscription<List<Group>>? _contactsSub;
|
||||||
|
StreamSubscription<List<Contact>>? _contactsCountSub;
|
||||||
List<Group> _groupsNotPinned = [];
|
List<Group> _groupsNotPinned = [];
|
||||||
List<Group> _groupsPinned = [];
|
List<Group> _groupsPinned = [];
|
||||||
List<Group> _groupsArchived = [];
|
List<Group> _groupsArchived = [];
|
||||||
|
|
||||||
|
bool _hasContacts = true;
|
||||||
|
bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || _groupsArchived.isNotEmpty || _groupsPinned.isNotEmpty;
|
||||||
|
|
||||||
GlobalKey searchForOtherUsers = GlobalKey();
|
GlobalKey searchForOtherUsers = GlobalKey();
|
||||||
bool showFeedbackShortcut = false;
|
bool showFeedbackShortcut = false;
|
||||||
|
|
||||||
|
|
@ -58,33 +63,34 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_contactsSub = stream.listen((groups) {
|
_contactsSub = stream.listen((groups) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_groupsNotPinned = groups
|
_groupsNotPinned = groups.where((x) => !x.pinned && !x.archived).toList();
|
||||||
.where((x) => !x.pinned && !x.archived)
|
|
||||||
.toList();
|
|
||||||
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
||||||
_groupsArchived = groups.where((x) => x.archived).toList();
|
_groupsArchived = groups.where((x) => x.archived).toList();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_countContactRequestStream = twonlyDB.contactsDao
|
_contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((contacts) {
|
||||||
.watchContactsRequestedCount()
|
if (!mounted) return;
|
||||||
.listen((update) {
|
setState(() {
|
||||||
if (update != null) {
|
_hasContacts = contacts.isNotEmpty;
|
||||||
if (!mounted) return;
|
});
|
||||||
setState(() {
|
});
|
||||||
_countContactRequest = update;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_countAnnouncedStream = twonlyDB.userDiscoveryDao
|
_countContactRequestStream = twonlyDB.contactsDao.watchContactsRequestedCount().listen((update) {
|
||||||
.watchNewAnnouncementsWithDataCount()
|
if (update != null) {
|
||||||
.listen((update) {
|
if (!mounted) return;
|
||||||
if (!mounted) return;
|
setState(() {
|
||||||
setState(() {
|
_countContactRequest = update;
|
||||||
_countAnnouncedUsers = update;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_countAnnouncedStream = twonlyDB.userDiscoveryDao.watchNewAnnouncementsWithDataCount().listen((update) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_countAnnouncedUsers = update;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||||
|
|
@ -93,8 +99,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
changeLog.codeUnits,
|
changeLog.codeUnits,
|
||||||
)).bytes;
|
)).bytes;
|
||||||
if (!userService.currentUser.hideChangeLog &&
|
if (!userService.currentUser.hideChangeLog &&
|
||||||
userService.currentUser.lastChangeLogHash.toString() !=
|
userService.currentUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||||
changeLogHash.toString()) {
|
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.lastChangeLogHash = changeLogHash;
|
u.lastChangeLogHash = changeLogHash;
|
||||||
});
|
});
|
||||||
|
|
@ -113,7 +118,8 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contactsSub.cancel();
|
_contactsSub?.cancel();
|
||||||
|
_contactsCountSub?.cancel();
|
||||||
_countContactRequestStream.cancel();
|
_countContactRequestStream.cancel();
|
||||||
_countAnnouncedStream.cancel();
|
_countAnnouncedStream.cancel();
|
||||||
_userSub?.cancel();
|
_userSub?.cancel();
|
||||||
|
|
@ -182,16 +188,11 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: NotificationBadgeComp(
|
child: NotificationBadgeComp(
|
||||||
backgroundColor: isDarkMode(context)
|
backgroundColor: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
count: (_countAnnouncedUsers + _countContactRequest)
|
count: (_countAnnouncedUsers + _countContactRequest).toString(),
|
||||||
.toString(),
|
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
color: (_countAnnouncedUsers + _countContactRequest > 0)
|
color: (_countAnnouncedUsers + _countContactRequest > 0) ? Colors.black : null,
|
||||||
? Colors.black
|
|
||||||
: null,
|
|
||||||
key: searchForOtherUsers,
|
key: searchForOtherUsers,
|
||||||
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
|
@ -217,21 +218,11 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
children: [
|
children: [
|
||||||
const FinishSetupComp(),
|
const FinishSetupComp(),
|
||||||
const MissingBackupComp(),
|
const MissingBackupComp(),
|
||||||
if (_groupsNotPinned.isEmpty &&
|
if (!_hasOpenGroup)
|
||||||
_groupsPinned.isEmpty &&
|
|
||||||
_groupsArchived.isEmpty)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: ListView(
|
||||||
child: Padding(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(10),
|
children: const [EmptyChatListComp()],
|
||||||
child: FilledButton.icon(
|
|
||||||
icon: const Icon(Icons.person_add),
|
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
|
||||||
label: Text(
|
|
||||||
context.lang.chatListViewSearchUserNameBtn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
@ -243,10 +234,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_groupsNotPinned.length +
|
_groupsNotPinned.length +
|
||||||
(_groupsArchived.isNotEmpty ? 1 : 0),
|
(_groupsArchived.isNotEmpty ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >=
|
if (index >= _groupsNotPinned.length + _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0)) {
|
||||||
_groupsNotPinned.length +
|
|
||||||
_groupsPinned.length +
|
|
||||||
(_groupsPinned.isNotEmpty ? 1 : 0)) {
|
|
||||||
if (_groupsArchived.isEmpty) return Container();
|
if (_groupsArchived.isEmpty) return Container();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
@ -289,42 +277,44 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: Padding(
|
floatingActionButton: !_hasContacts
|
||||||
padding: const EdgeInsets.only(bottom: 30),
|
? null
|
||||||
child: Column(
|
: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
children: [
|
child: Column(
|
||||||
Material(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
elevation: 3,
|
children: [
|
||||||
shape: const CircleBorder(),
|
Material(
|
||||||
color: context.color.primary,
|
elevation: 3,
|
||||||
child: InkWell(
|
shape: const CircleBorder(),
|
||||||
borderRadius: BorderRadius.circular(12),
|
color: context.color.primary,
|
||||||
onTap: () => context.push(Routes.settingsPublicProfile),
|
child: InkWell(
|
||||||
child: SizedBox(
|
borderRadius: BorderRadius.circular(12),
|
||||||
width: 45,
|
onTap: () => context.push(Routes.settingsPublicProfile),
|
||||||
height: 45,
|
child: SizedBox(
|
||||||
child: Center(
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
child: Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.qrcode,
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FloatingActionButton(
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
onPressed: () => context.push(Routes.chatsStartNewChat),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.qrcode,
|
FontAwesomeIcons.penToSquare,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
FloatingActionButton(
|
|
||||||
backgroundColor: context.color.primary,
|
|
||||||
onPressed: () => context.push(Routes.chatsStartNewChat),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.penToSquare,
|
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart' show FaIcon, FontAwesomeIcons;
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
class EmptyChatListComp extends StatelessWidget {
|
||||||
|
const EmptyChatListComp({super.key});
|
||||||
|
|
||||||
|
Future<void> _shareProfile(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
final pubKey = await getUserPublicKey();
|
||||||
|
final params = ShareParams(
|
||||||
|
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||||
|
);
|
||||||
|
await SharePlus.instance.share(params);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await context.push(Routes.chatsAddNewUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: double.infinity,
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Find your first friend',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Let friends scan your QR code, or share them your profile.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: context.color.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
const Center(child: ProfileQrCodeComp()),
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
// 3. Action Buttons
|
||||||
|
// Button 1: Share Profile (Full Width)
|
||||||
|
FilledButton.icon(
|
||||||
|
style: primaryColorButtonStyle,
|
||||||
|
onPressed: () => _shareProfile(context),
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.shareNodes, size: 20),
|
||||||
|
label: const Text(
|
||||||
|
'Share your profile',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Button Row: Scan QR Code & Enter Username
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: secondaryGreyButtonStyle(context),
|
||||||
|
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner_rounded, size: 20),
|
||||||
|
label: const FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
'Scan QR Code',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: secondaryGreyButtonStyle(context),
|
||||||
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
icon: const Icon(Icons.person_add_rounded, size: 20),
|
||||||
|
label: const FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
'Add by Username',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,24 +62,21 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
|
||||||
.watchNewAnnouncedUsersWithRelations()
|
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) {
|
||||||
.listen((update) {
|
if (mounted) {
|
||||||
if (mounted) {
|
setState(() {
|
||||||
setState(() {
|
_newAnnouncedUsers = update;
|
||||||
_newAnnouncedUsers = update;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
}
|
||||||
.watchAllAnnouncedUsersWithRelations()
|
});
|
||||||
.listen((update) {
|
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_allAnnouncedUsers = update;
|
_allAnnouncedUsers = update;
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (widget.username != null) {
|
if (widget.username != null) {
|
||||||
_usernameController.text = widget.username!;
|
_usernameController.text = widget.username!;
|
||||||
|
|
@ -93,8 +90,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
Future<void> _shareProfile() async {
|
Future<void> _shareProfile() async {
|
||||||
final pubKey = await getUserPublicKey();
|
final pubKey = await getUserPublicKey();
|
||||||
final params = ShareParams(
|
final params = ShareParams(
|
||||||
text:
|
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||||
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
|
||||||
);
|
);
|
||||||
await SharePlus.instance.share(params);
|
await SharePlus.instance.share(params);
|
||||||
}
|
}
|
||||||
|
|
@ -194,9 +190,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.publicKey != null &&
|
if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
||||||
mounted &&
|
|
||||||
widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
|
||||||
final markAsVerified = await showAlertDialog(
|
final markAsVerified = await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
context.lang.linkFromUsername(username),
|
context.lang.linkFromUsername(username),
|
||||||
|
|
@ -321,15 +315,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
FontAwesomeIcons.shareNodes,
|
FontAwesomeIcons.shareNodes,
|
||||||
size: 14,
|
size: 14,
|
||||||
),
|
),
|
||||||
label: FittedBox(
|
label: Text(
|
||||||
fit: BoxFit.scaleDown,
|
context.lang.shareYourProfile,
|
||||||
child: Text(
|
style: const TextStyle(fontSize: 13),
|
||||||
context.lang.shareYourProfile,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -353,15 +341,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
FontAwesomeIcons.qrcode,
|
FontAwesomeIcons.qrcode,
|
||||||
size: 14,
|
size: 14,
|
||||||
),
|
),
|
||||||
label: FittedBox(
|
label: Text(
|
||||||
fit: BoxFit.scaleDown,
|
context.lang.openYourOwnQRcode,
|
||||||
child: Text(
|
style: const TextStyle(fontSize: 13),
|
||||||
context.lang.openYourOwnQRcode,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -371,11 +353,18 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
OpenRequestsListComp(
|
Padding(
|
||||||
contacts: _openRequestsContacts,
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
relations: _allAnnouncedUsers,
|
child: Column(
|
||||||
|
children: [
|
||||||
|
OpenRequestsListComp(
|
||||||
|
contacts: _openRequestsContacts,
|
||||||
|
relations: _allAnnouncedUsers,
|
||||||
|
),
|
||||||
|
FriendSuggestionsComp(_newAnnouncedUsers),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
FriendSuggestionsComp(_newAnnouncedUsers),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/headline.element.dart';
|
import 'package:twonly/src/visual/elements/headline.element.dart';
|
||||||
|
|
@ -63,8 +64,17 @@ class OpenRequestsListComp extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
const update = ContactsCompanion(blocked: Value(true));
|
final block = await showAlertDialog(
|
||||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
context,
|
||||||
|
context.lang.contactBlockTitle(getContactDisplayName(contact)),
|
||||||
|
context.lang.contactBlockBody,
|
||||||
|
);
|
||||||
|
if (block) {
|
||||||
|
const update = ContactsCompanion(blocked: Value(true));
|
||||||
|
if (context.mounted) {
|
||||||
|
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -179,9 +189,7 @@ class OpenRequestsListComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: contact.requested
|
children: contact.requested ? requestedActions(context, contact) : sendRequestActions(context, contact),
|
||||||
? requestedActions(context, contact)
|
|
||||||
: sendRequestActions(context, contact),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
_selectNotificationSub = selectNotificationStream.stream.listen((
|
_selectNotificationSub = selectNotificationStream.stream.listen((
|
||||||
response,
|
response,
|
||||||
) async {
|
) async {
|
||||||
if (response.payload != null &&
|
if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) {
|
||||||
response.payload!.startsWith(Routes.chats) &&
|
|
||||||
response.payload! != Routes.chats) {
|
|
||||||
await routerProvider.push(response.payload!);
|
await routerProvider.push(response.payload!);
|
||||||
}
|
}
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
|
|
@ -116,40 +114,31 @@ class HomeViewState extends State<HomeView> {
|
||||||
);
|
);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (widget.initialPage == 1 &&
|
if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) {
|
||||||
!userService.currentUser.startWithCameraOpen ||
|
|
||||||
widget.initialPage == 0) {
|
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initAsync() async {
|
Future<void> _initAsync() async {
|
||||||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||||
.getNotificationAppLaunchDetails();
|
|
||||||
|
|
||||||
RemoteMessage? initialRemoteMessage;
|
RemoteMessage? initialRemoteMessage;
|
||||||
try {
|
try {
|
||||||
initialRemoteMessage = await FirebaseMessaging.instance
|
initialRemoteMessage = await FirebaseMessaging.instance.getInitialMessage();
|
||||||
.getInitialMessage();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not get initial Firebase message: $e');
|
Log.error('Could not get initial Firebase message: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.initialPage == 0 ||
|
if (widget.initialPage == 0 ||
|
||||||
initialRemoteMessage != null ||
|
initialRemoteMessage != null ||
|
||||||
(notificationAppLaunchDetails != null &&
|
(notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
||||||
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
|
||||||
if (initialRemoteMessage != null) {
|
if (initialRemoteMessage != null) {
|
||||||
Log.info('App launched from iOS/Remote push notification tap.');
|
Log.info('App launched from iOS/Remote push notification tap.');
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ??
|
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
false) {
|
final payload = notificationAppLaunchDetails?.notificationResponse?.payload;
|
||||||
final payload =
|
if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) {
|
||||||
notificationAppLaunchDetails?.notificationResponse?.payload;
|
|
||||||
if (payload != null &&
|
|
||||||
payload.startsWith(Routes.chats) &&
|
|
||||||
payload != Routes.chats) {
|
|
||||||
await routerProvider.push(payload);
|
await routerProvider.push(payload);
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
}
|
}
|
||||||
|
|
@ -190,25 +179,30 @@ class HomeViewState extends State<HomeView> {
|
||||||
_disableCameraTimer?.cancel();
|
_disableCameraTimer?.cancel();
|
||||||
|
|
||||||
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
||||||
if (_activePageIdx == 2 &&
|
final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent;
|
||||||
notification.metrics.pixels < 100 &&
|
if (!canScroll) {
|
||||||
!_isBottomNavVisible) {
|
if (!_isBottomNavVisible) {
|
||||||
setState(() {
|
|
||||||
_isBottomNavVisible = true;
|
|
||||||
});
|
|
||||||
} else if (notification is ScrollUpdateNotification) {
|
|
||||||
final delta = notification.scrollDelta ?? 0;
|
|
||||||
if (delta > 5 &&
|
|
||||||
_isBottomNavVisible &&
|
|
||||||
(_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
|
||||||
setState(() {
|
|
||||||
_isBottomNavVisible = false;
|
|
||||||
});
|
|
||||||
} else if (delta < -5 && !_isBottomNavVisible) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBottomNavVisible = true;
|
_isBottomNavVisible = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = true;
|
||||||
|
});
|
||||||
|
} else if (notification is ScrollUpdateNotification) {
|
||||||
|
final delta = notification.scrollDelta ?? 0;
|
||||||
|
if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = false;
|
||||||
|
});
|
||||||
|
} else if (delta < -5 && !_isBottomNavVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,9 +238,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onDoubleTap: _offsetRatio == 0
|
onDoubleTap: _offsetRatio == 0 ? _mainCameraController.onDoubleTap : null,
|
||||||
? _mainCameraController.onDoubleTap
|
|
||||||
: null,
|
|
||||||
onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|
@ -281,16 +273,12 @@ class HomeViewState extends State<HomeView> {
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: (_offsetRatio > 0.25)
|
bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0,
|
||||||
? MediaQuery.sizeOf(context).height * 2
|
|
||||||
: 0,
|
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: 1 - (_offsetRatio * 4) % 1,
|
opacity: 1 - (_offsetRatio * 4) % 1,
|
||||||
child: CameraPreviewControllerView(
|
child: CameraPreviewControllerView(
|
||||||
mainController: _mainCameraController,
|
mainController: _mainCameraController,
|
||||||
isVisible:
|
isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1,
|
||||||
((1 - (_offsetRatio * 4) % 1) == 1) &&
|
|
||||||
_activePageIdx == 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -153,21 +153,19 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
final isDark = isDarkMode(context);
|
final isDark = isDarkMode(context);
|
||||||
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||||
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
|
||||||
final sloganColor = isDark
|
final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800];
|
||||||
? Colors.white.withValues(alpha: 0.9)
|
|
||||||
: Colors.grey[800];
|
|
||||||
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
|
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
|
||||||
|
|
||||||
return OnboardingWrapper(
|
return OnboardingWrapper(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 30),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(10),
|
||||||
child: const LinkLogoAnimation(),
|
child: const LinkLogoAnimation(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -180,7 +178,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 30),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -188,9 +186,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: isDark
|
color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1),
|
||||||
? Colors.black.withValues(alpha: 0.3)
|
|
||||||
: Colors.black.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
|
|
@ -262,8 +258,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_showUserNameError &&
|
if (_showUserNameError && usernameController.text.length < 3) ...[
|
||||||
usernameController.text.length < 3) ...[
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
context.lang.registerUsernameLimits,
|
context.lang.registerUsernameLimits,
|
||||||
|
|
|
||||||
|
|
@ -10,58 +10,64 @@ class MockContactRequestActionsComp extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
// width: 125,
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
child: FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.only(right: 2, left: 4),
|
||||||
|
backgroundColor: context.color.surfaceContainerHigh,
|
||||||
|
foregroundColor: context.color.onSurface,
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.person_off_rounded,
|
||||||
|
color: Color.fromARGB(164, 244, 67, 54),
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.lang.contactActionBlock,
|
||||||
|
style: const TextStyle(fontSize: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
child: FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.only(right: 2, left: 4),
|
||||||
|
backgroundColor: context.color.surfaceContainerHigh,
|
||||||
|
foregroundColor: context.color.onSurface,
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check, color: Colors.green, size: 12),
|
||||||
|
Text(
|
||||||
|
context.lang.contactActionAccept,
|
||||||
|
style: const TextStyle(fontSize: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
SizedBox(
|
|
||||||
height: 20,
|
|
||||||
// width: 45,
|
|
||||||
child: FilledButton(
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.only(right: 2, left: 4),
|
|
||||||
backgroundColor: context.color.surfaceContainerHigh,
|
|
||||||
foregroundColor: context.color.onSurface,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.person_off_rounded,
|
|
||||||
color: Color.fromARGB(164, 244, 67, 54),
|
|
||||||
size: 12,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
context.lang.contactActionBlock,
|
|
||||||
style: const TextStyle(fontSize: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
SizedBox(
|
|
||||||
height: 20,
|
|
||||||
// width: 50,
|
|
||||||
child: FilledButton(
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.only(right: 2, left: 4),
|
|
||||||
backgroundColor: context.color.surfaceContainerHigh,
|
|
||||||
foregroundColor: context.color.onSurface,
|
|
||||||
),
|
|
||||||
onPressed: () {},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.check, color: Colors.green, size: 12),
|
|
||||||
Text(
|
|
||||||
context.lang.contactActionAccept,
|
|
||||||
style: const TextStyle(fontSize: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
|
@ -86,28 +92,59 @@ class MockContactSuggestedActionsComp extends StatelessWidget {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
SizedBox(
|
Column(
|
||||||
height: 20,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: FilledButton(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
style: FilledButton.styleFrom(
|
children: [
|
||||||
padding: const EdgeInsets.only(right: 8, left: 4),
|
SizedBox(
|
||||||
).merge(secondaryGreyButtonStyle(context)),
|
height: 20,
|
||||||
onPressed: () {},
|
child: FilledButton(
|
||||||
child: Row(
|
style: FilledButton.styleFrom(
|
||||||
children: [
|
padding: const EdgeInsets.only(right: 8, left: 4),
|
||||||
const Padding(
|
).merge(secondaryGreyButtonStyle(context)),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
onPressed: () {},
|
||||||
child: FaIcon(FontAwesomeIcons.userPlus, size: 10),
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: FaIcon(FontAwesomeIcons.circleQuestion, size: 10),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.lang.friendSuggestionsAskFriend,
|
||||||
|
style: const TextStyle(fontSize: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
context.lang.friendSuggestionsRequest,
|
|
||||||
style: const TextStyle(fontSize: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
|
SizedBox(
|
||||||
|
height: 20,
|
||||||
|
child: FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.only(right: 8, left: 4),
|
||||||
|
).merge(secondaryGreyButtonStyle(context)),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: FaIcon(FontAwesomeIcons.userPlus, size: 10),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.lang.friendSuggestionsRequest,
|
||||||
|
style: const TextStyle(fontSize: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,9 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/contact_request_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
import 'package:twonly/src/visual/components/profile_qr_code.comp.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/themes/light.dart';
|
|
||||||
|
|
||||||
class PublicProfileView extends StatefulWidget {
|
class PublicProfileView extends StatefulWidget {
|
||||||
const PublicProfileView({super.key});
|
const PublicProfileView({super.key});
|
||||||
|
|
@ -24,8 +23,6 @@ class PublicProfileView extends StatefulWidget {
|
||||||
|
|
||||||
class _PublicProfileViewState extends State<PublicProfileView> {
|
class _PublicProfileViewState extends State<PublicProfileView> {
|
||||||
Uint8List? _publicKey;
|
Uint8List? _publicKey;
|
||||||
int _countContactRequest = 0;
|
|
||||||
late StreamSubscription<int?> _countContactRequestStream;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -36,70 +33,15 @@ class _PublicProfileViewState extends State<PublicProfileView> {
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_publicKey = await getUserPublicKey();
|
_publicKey = await getUserPublicKey();
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
|
|
||||||
_countContactRequestStream = twonlyDB.contactsDao
|
|
||||||
.watchContactsRequestedCount()
|
|
||||||
.listen((update) {
|
|
||||||
if (update != null) {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_countContactRequest = update;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_countContactRequestStream.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
actions: [
|
actions: const [
|
||||||
Stack(
|
ContactRequestBadgeComp(),
|
||||||
children: (_countContactRequest == 0)
|
SizedBox(width: 15),
|
||||||
? []
|
|
||||||
: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: primaryColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: NotificationBadgeComp(
|
|
||||||
backgroundColor: isDarkMode(context)
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
textColor: isDarkMode(context)
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
count: (_countContactRequest).toString(),
|
|
||||||
child: IconButton(
|
|
||||||
color: (_countContactRequest > 0)
|
|
||||||
? Colors.black
|
|
||||||
: null,
|
|
||||||
icon: const FaIcon(
|
|
||||||
FontAwesomeIcons.userPlus,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
|
@ -155,8 +97,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final params = ShareParams(
|
final params = ShareParams(
|
||||||
text:
|
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}',
|
||||||
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}',
|
|
||||||
);
|
);
|
||||||
SharePlus.instance.share(params);
|
SharePlus.instance.share(params);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final showShareYourFriends =
|
final showShareYourFriends =
|
||||||
showOnlySpecificPage == UserDiscoveryPages.all ||
|
showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.shareYourFriends;
|
||||||
showOnlySpecificPage == UserDiscoveryPages.shareYourFriends;
|
|
||||||
final showLetYourFriendsFindYou =
|
final showLetYourFriendsFindYou =
|
||||||
showOnlySpecificPage == UserDiscoveryPages.all ||
|
showOnlySpecificPage == UserDiscoveryPages.all ||
|
||||||
showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou;
|
showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou;
|
||||||
|
|
@ -172,7 +171,6 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
|
|
@ -334,9 +332,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
context
|
context.lang.userDiscoverySettingsManualApprovalDesc,
|
||||||
.lang
|
|
||||||
.userDiscoverySettingsManualApprovalDesc,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: context.color.onSurfaceVariant,
|
color: context.color.onSurfaceVariant,
|
||||||
|
|
@ -350,16 +346,13 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
crossFadeState: state.isUserDiscoveryEnabled
|
crossFadeState: state.isUserDiscoveryEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||||
? CrossFadeState.showSecond
|
|
||||||
: CrossFadeState.showFirst,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showOnlySpecificPage == UserDiscoveryPages.all)
|
if (showOnlySpecificPage == UserDiscoveryPages.all) const SizedBox(height: 48),
|
||||||
const SizedBox(height: 48),
|
|
||||||
],
|
],
|
||||||
if (showLetYourFriendsFindYou) ...[
|
if (showLetYourFriendsFindYou) ...[
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -418,7 +411,6 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
|
|
@ -483,7 +475,6 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
|
|
@ -592,9 +583,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
context
|
context.lang.userDiscoverySettingsMutualFriends,
|
||||||
.lang
|
|
||||||
.userDiscoverySettingsMutualFriends,
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -610,10 +599,9 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
color: context.color.surface,
|
color: context.color.surface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.color.outlineVariant
|
color: context.color.outlineVariant.withValues(
|
||||||
.withValues(
|
alpha: 0.5,
|
||||||
alpha: 0.5,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
|
|
@ -648,9 +636,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
crossFadeState: state.sharePromotion
|
crossFadeState: state.sharePromotion ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||||
? CrossFadeState.showSecond
|
|
||||||
: CrossFadeState.showFirst,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue