mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-27 14:59:29 +01:00
feat: New material 3 design
This commit is contained in:
parent
802ff0fa9d
commit
091958be0b
@ -2826,5 +2826,7 @@
|
||||
"user": {}
|
||||
}
|
||||
},
|
||||
"noEmailWarning": "Please enter a valid email address. Otherwise you won't be able to reset your password. If you don't want to, tap again on the button to continue."
|
||||
"noEmailWarning": "Please enter a valid email address. Otherwise you won't be able to reset your password. If you don't want to, tap again on the button to continue.",
|
||||
"stories": "Stories",
|
||||
"users": "Users"
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import 'package:fluffychat/pages/login/login.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart';
|
||||
import 'package:fluffychat/pages/new_space/new_space.dart';
|
||||
import 'package:fluffychat/pages/search/search.dart';
|
||||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart';
|
||||
import 'package:fluffychat/pages/settings_account/settings_account.dart';
|
||||
@ -92,10 +91,6 @@ class AppRoutes {
|
||||
widget: const Settings(),
|
||||
stackedRoutes: _settingsRoutes,
|
||||
),
|
||||
VWidget(
|
||||
path: '/search',
|
||||
widget: const Search(),
|
||||
),
|
||||
VWidget(
|
||||
path: '/archive',
|
||||
widget: const Archive(),
|
||||
@ -225,14 +220,6 @@ class AppRoutes {
|
||||
),
|
||||
],
|
||||
),
|
||||
VWidget(
|
||||
path: '/search',
|
||||
widget: const TwoColumnLayout(
|
||||
mainView: Search(),
|
||||
sideView: EmptyPage(),
|
||||
),
|
||||
buildTransition: _fadeTransition,
|
||||
),
|
||||
VWidget(
|
||||
path: '/archive',
|
||||
widget: const TwoColumnLayout(
|
||||
|
@ -9,56 +9,10 @@ abstract class FluffyThemes {
|
||||
static bool isColumnMode(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width > columnWidth * 2;
|
||||
|
||||
static const fallbackTextStyle =
|
||||
TextStyle(fontFamily: 'Roboto', fontFamilyFallback: ['NotoEmoji']);
|
||||
|
||||
static const TextStyle loginTextFieldStyle = TextStyle(color: Colors.black);
|
||||
|
||||
static InputDecoration loginTextFieldDecoration({
|
||||
String? errorText,
|
||||
String? labelText,
|
||||
String? hintText,
|
||||
Widget? suffixIcon,
|
||||
Widget? prefixIcon,
|
||||
Color? errorColor,
|
||||
}) =>
|
||||
InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
fillColor: Colors.white.withAlpha(200),
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
suffixIcon: suffixIcon,
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIconColor: Colors.black,
|
||||
prefixIconColor: Colors.black,
|
||||
iconColor: Colors.black,
|
||||
errorText: errorText,
|
||||
errorMaxLines: 4,
|
||||
errorStyle: TextStyle(
|
||||
color: errorColor ?? Colors.redAccent.shade200,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
hintStyle: TextStyle(color: Colors.grey.shade700),
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
);
|
||||
static const fallbackTextStyle = TextStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontFamilyFallback: ['NotoEmoji'],
|
||||
);
|
||||
|
||||
static var fallbackTextTheme = const TextTheme(
|
||||
bodyText1: fallbackTextStyle,
|
||||
@ -83,12 +37,12 @@ abstract class FluffyThemes {
|
||||
colorSchemeSeed: AppConfig.colorSchemeSeed ??
|
||||
colorScheme?.primary ??
|
||||
AppConfig.chatColor,
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
textTheme: PlatformInfos.isDesktop
|
||||
? Typography.material2018().black.merge(fallbackTextTheme)
|
||||
: null,
|
||||
snackBarTheme:
|
||||
const SnackBarThemeData(behavior: SnackBarBehavior.floating),
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
|
||||
@ -100,31 +54,12 @@ abstract class FluffyThemes {
|
||||
},
|
||||
),
|
||||
dividerColor: Colors.blueGrey.shade50,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 16),
|
||||
elevation: 6,
|
||||
shadowColor: const Color(0x44000000),
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
cardTheme: const CardTheme(
|
||||
elevation: 6,
|
||||
// shadowColor: Color(0x44000000),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: const UnderlineInputBorder(borderSide: BorderSide(width: 1)),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: UnderlineInputBorder(borderSide: BorderSide(width: 1)),
|
||||
filled: true,
|
||||
fillColor: Colors.blueGrey.shade50,
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 6,
|
||||
shadowColor: Color(0x44000000),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
surfaceTintColor: Colors.white,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
);
|
||||
|
||||
@ -135,7 +70,6 @@ abstract class FluffyThemes {
|
||||
colorSchemeSeed: AppConfig.colorSchemeSeed ??
|
||||
colorScheme?.primary ??
|
||||
AppConfig.chatColor,
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
textTheme: PlatformInfos.isDesktop
|
||||
? Typography.material2018().white.merge(fallbackTextTheme)
|
||||
: null,
|
||||
@ -151,20 +85,11 @@ abstract class FluffyThemes {
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
dividerColor: Colors.blueGrey.shade600,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: AppConfig.chatColor,
|
||||
onPrimary: Colors.white,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
textStyle: const TextStyle(fontSize: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 6,
|
||||
backgroundColor: Color(0xff1D1D1D),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: UnderlineInputBorder(borderSide: BorderSide(width: 1)),
|
||||
filled: true,
|
||||
),
|
||||
dividerColor: Colors.blueGrey.shade900,
|
||||
);
|
||||
|
||||
static Color blackWhiteColor(BuildContext context) =>
|
||||
|
@ -255,12 +255,13 @@ class ChatView extends StatelessWidget {
|
||||
),
|
||||
elevation: 6,
|
||||
shadowColor: Theme.of(context)
|
||||
.secondaryHeaderColor
|
||||
.dividerColor
|
||||
.withAlpha(100),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.backgroundColor,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -68,7 +68,9 @@ class Message extends StatelessWidget {
|
||||
final client = Matrix.of(context).client;
|
||||
final ownMessage = event.senderId == client.userID;
|
||||
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
|
||||
var color = Theme.of(context).scaffoldBackgroundColor;
|
||||
var color = Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
final displayTime = event.type == EventTypes.RoomCreate ||
|
||||
nextEvent == null ||
|
||||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
|
||||
|
@ -14,9 +14,9 @@ import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/famedlysdk_store.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import '../../../utils/account_bundles.dart';
|
||||
import '../../main.dart';
|
||||
@ -52,6 +52,97 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
|
||||
SpacesEntry? _activeSpacesEntry;
|
||||
|
||||
bool isSearchMode = false;
|
||||
Future<QueryPublicRoomsResponse>? publicRoomsResponse;
|
||||
String? searchServer;
|
||||
Timer? _coolDown;
|
||||
SearchUserDirectoryResponse? userSearchResult;
|
||||
QueryPublicRoomsResponse? roomSearchResult;
|
||||
|
||||
bool isSearching = false;
|
||||
static const String _serverStoreNamespace = 'im.fluffychat.search.server';
|
||||
|
||||
void setServer() async {
|
||||
final newServer = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context)!.changeTheHomeserver,
|
||||
context: context,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
prefixText: 'https://',
|
||||
hintText: Matrix.of(context).client.homeserver?.host,
|
||||
initialText: searchServer,
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false)
|
||||
]);
|
||||
if (newServer == null) return;
|
||||
Store().setItem(_serverStoreNamespace, newServer.single);
|
||||
setState(() {
|
||||
searchServer = newServer.single;
|
||||
});
|
||||
onSearchEnter(searchController.text);
|
||||
}
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
void _search() async {
|
||||
final client = Matrix.of(context).client;
|
||||
if (!isSearching) {
|
||||
setState(() {
|
||||
isSearching = true;
|
||||
});
|
||||
}
|
||||
SearchUserDirectoryResponse? userSearchResult;
|
||||
QueryPublicRoomsResponse? roomSearchResult;
|
||||
try {
|
||||
roomSearchResult = await client.queryPublicRooms(
|
||||
server: searchServer,
|
||||
filter: PublicRoomQueryFilter(genericSearchTerm: searchController.text),
|
||||
limit: 20,
|
||||
);
|
||||
userSearchResult = await client.searchUserDirectory(
|
||||
searchController.text,
|
||||
limit: 20,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logs().w('Searching has crashed', e, s);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
e.toLocalizedString(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
isSearching = false;
|
||||
this.roomSearchResult = roomSearchResult;
|
||||
this.userSearchResult = userSearchResult;
|
||||
});
|
||||
}
|
||||
|
||||
void onSearchEnter(String text) {
|
||||
if (text.isEmpty) {
|
||||
cancelSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isSearchMode = true;
|
||||
});
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(const Duration(milliseconds: 500), _search);
|
||||
}
|
||||
|
||||
void cancelSearch() => setState(() {
|
||||
searchController.clear();
|
||||
isSearchMode = false;
|
||||
roomSearchResult = userSearchResult = null;
|
||||
isSearching = false;
|
||||
});
|
||||
|
||||
SpacesEntry get activeSpacesEntry {
|
||||
final id = _activeSpacesEntry;
|
||||
return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id;
|
||||
@ -72,6 +163,8 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
|
||||
Stream<Client> get clientStream => _clientStream.stream;
|
||||
|
||||
void addAccountAction() => VRouter.of(context).to('/settings/account/add');
|
||||
|
||||
void _onScroll() {
|
||||
final newScrolledToTop = scrollController.position.pixels <= 0;
|
||||
if (newScrolledToTop != scrolledToTop) {
|
||||
@ -82,12 +175,7 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
void setActiveSpacesEntry(BuildContext context, SpacesEntry? spaceId) {
|
||||
if ((snappingSheetController.isAttached
|
||||
? snappingSheetController.currentPosition
|
||||
: 0) !=
|
||||
kSpacesBottomBarHeight) {
|
||||
snapBackSpacesSheet();
|
||||
}
|
||||
Scaffold.of(context).closeDrawer();
|
||||
setState(() => _activeSpacesEntry = spaceId);
|
||||
}
|
||||
|
||||
@ -212,6 +300,10 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
scrollController.addListener(_onScroll);
|
||||
_waitForFirstSync();
|
||||
_hackyWebRTCFixForWeb();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
searchServer = await Store().getItem(_serverStoreNamespace);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -338,32 +430,6 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
|
||||
void onPopupMenuSelect(action) {
|
||||
switch (action) {
|
||||
case PopupMenuAction.setStatus:
|
||||
setStatus();
|
||||
break;
|
||||
case PopupMenuAction.settings:
|
||||
VRouter.of(context).to('/settings');
|
||||
break;
|
||||
case PopupMenuAction.invite:
|
||||
FluffyShare.share(
|
||||
L10n.of(context)!.inviteText(Matrix.of(context).client.userID!,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'),
|
||||
context);
|
||||
break;
|
||||
case PopupMenuAction.newGroup:
|
||||
VRouter.of(context).to('/newgroup');
|
||||
break;
|
||||
case PopupMenuAction.newSpace:
|
||||
VRouter.of(context).to('/newspace');
|
||||
break;
|
||||
case PopupMenuAction.archive:
|
||||
VRouter.of(context).to('/archive');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _archiveSelectedRooms() async {
|
||||
final client = Matrix.of(context).client;
|
||||
while (selectedRoomIds.isNotEmpty) {
|
||||
@ -593,15 +659,6 @@ class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
Matrix.of(context).voipPlugin?.context = context;
|
||||
}
|
||||
|
||||
void snapBackSpacesSheet() {
|
||||
snappingSheetController.snapToPosition(
|
||||
const SnappingPosition.pixels(
|
||||
positionPixels: kSpacesBottomBarHeight,
|
||||
snappingDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
expandSpaces() {
|
||||
snappingSheetController.snapToPosition(
|
||||
const SnappingPosition.factor(positionFactor: 0.5),
|
||||
|
@ -8,9 +8,12 @@ import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
|
||||
import 'package:fluffychat/pages/chat_list/stories_header.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/widgets/profile_bottom_sheet.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import '../../utils/stream_extension.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
@ -46,6 +49,8 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reversed = !_animationReversed();
|
||||
final roomSearchResult = widget.controller.roomSearchResult;
|
||||
final userSearchResult = widget.controller.userSearchResult;
|
||||
Widget child;
|
||||
if (widget.controller.waitForFirstSync &&
|
||||
Matrix.of(context).client.prevBatch != null) {
|
||||
@ -86,13 +91,113 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (displayStoriesHeader) {
|
||||
if (i == 0) {
|
||||
return const StoriesHeader();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
if (roomSearchResult != null) ...[
|
||||
_SearchTitle(title: L10n.of(context)!.publicRooms),
|
||||
AnimatedContainer(
|
||||
height: roomSearchResult.chunk.isEmpty ? 0 : 106,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: roomSearchResult.chunk.length,
|
||||
itemBuilder: (context, i) => _SearchItem(
|
||||
title: roomSearchResult.chunk[i].name ??
|
||||
roomSearchResult
|
||||
.chunk[i].canonicalAlias?.localpart ??
|
||||
L10n.of(context)!.group,
|
||||
avatar: roomSearchResult.chunk[i].avatarUrl,
|
||||
onPressed: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => PublicRoomBottomSheet(
|
||||
roomAlias:
|
||||
roomSearchResult.chunk[i].canonicalAlias ??
|
||||
roomSearchResult.chunk[i].roomId,
|
||||
outerContext: context,
|
||||
chunk: roomSearchResult.chunk[i],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (userSearchResult != null) ...[
|
||||
_SearchTitle(title: L10n.of(context)!.users),
|
||||
AnimatedContainer(
|
||||
height: userSearchResult.results.isEmpty ? 0 : 106,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: userSearchResult.results.length,
|
||||
itemBuilder: (context, i) => _SearchItem(
|
||||
title: userSearchResult.results[i].displayName ??
|
||||
userSearchResult.results[i].userId.localpart ??
|
||||
L10n.of(context)!.unknownDevice,
|
||||
avatar: userSearchResult.results[i].avatarUrl,
|
||||
onPressed: () => showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => ProfileBottomSheet(
|
||||
userId: userSearchResult.results[i].userId,
|
||||
outerContext: context,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.controller.isSearchMode)
|
||||
_SearchTitle(title: L10n.of(context)!.stories),
|
||||
StoriesHeader(
|
||||
filter: widget.controller.searchController.text,
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: !widget.controller.isSearchMode &&
|
||||
widget.controller.showChatBackupBanner
|
||||
? 54
|
||||
: 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
curve: Curves.bounceInOut,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListTile(
|
||||
leading: Image.asset(
|
||||
'assets/backup.png',
|
||||
fit: BoxFit.contain,
|
||||
width: 44,
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.setupChatBackupNow,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: widget.controller.firstRunBootstrapAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.controller.isSearchMode)
|
||||
_SearchTitle(title: L10n.of(context)!.chats),
|
||||
],
|
||||
);
|
||||
}
|
||||
i--;
|
||||
}
|
||||
if (i >= rooms.length) {
|
||||
return const ListTile();
|
||||
}
|
||||
if (!rooms[i].displayname.toLowerCase().contains(
|
||||
widget.controller.searchController.text.toLowerCase())) {
|
||||
return Container();
|
||||
}
|
||||
return ChatListItem(
|
||||
rooms[i],
|
||||
selected: widget.controller.selectedRoomIds.contains(rooms[i].id),
|
||||
@ -176,13 +281,7 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: (widget.controller.snappingSheetController.isAttached
|
||||
? widget
|
||||
.controller.snappingSheetController.currentPosition
|
||||
: 0) ==
|
||||
kSpacesBottomBarHeight
|
||||
? SharedAxisTransitionType.horizontal
|
||||
: SharedAxisTransitionType.vertical,
|
||||
transitionType: SharedAxisTransitionType.vertical,
|
||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
@ -221,3 +320,77 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
|
||||
return reversed;
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchTitle extends StatelessWidget {
|
||||
final String title;
|
||||
const _SearchTitle({required this.title, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
)),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(title,
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _SearchItem extends StatelessWidget {
|
||||
final String title;
|
||||
final Uri? avatar;
|
||||
final void Function() onPressed;
|
||||
const _SearchItem({
|
||||
required this.title,
|
||||
this.avatar,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => InkWell(
|
||||
onTap: onPressed,
|
||||
child: SizedBox(
|
||||
width: 84,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Avatar(
|
||||
mxContent: avatar,
|
||||
name: title,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
67
lib/pages/chat_list/chat_list_drawer.dart
Normal file
67
lib/pages/chat_list/chat_list_drawer.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_drawer.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ChatListDrawer extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
const ChatListDrawer(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Drawer(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SpacesDrawer(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.group_add_outlined,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
title: Text(L10n.of(context)!.createNewGroup),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
VRouter.of(context).to('/newgroup');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.adaptive.share_outlined,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
title: Text(L10n.of(context)!.inviteContact),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
FluffyShare.share(
|
||||
L10n.of(context)!.inviteText(
|
||||
Matrix.of(context).client.userID!,
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'),
|
||||
context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.settings_outlined,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
title: Text(L10n.of(context)!.settings),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
VRouter.of(context).to('/settings');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,15 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ChatListController controller;
|
||||
@ -21,23 +17,92 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final selectMode = controller.selectMode;
|
||||
|
||||
return AppBar(
|
||||
elevation: controller.scrolledToTop ? 0 : null,
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedRoomIds.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: Matrix.of(context).isMultiAccount
|
||||
? ClientChooserButton(controller)
|
||||
: selectMode == SelectMode.normal
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
titleSpacing: 8,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: selectMode == SelectMode.normal
|
||||
? null
|
||||
: IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelAction,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
title: selectMode == SelectMode.share
|
||||
? Text(
|
||||
L10n.of(context)!.share,
|
||||
key: const ValueKey(SelectMode.share),
|
||||
)
|
||||
: selectMode == SelectMode.select
|
||||
? Text(
|
||||
controller.selectedRoomIds.length.toString(),
|
||||
key: const ValueKey(SelectMode.select),
|
||||
)
|
||||
: TextField(
|
||||
controller: controller.searchController,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: controller.onSearchEnter,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
hintText: L10n.of(context)!.search,
|
||||
prefixIcon: controller.isSearchMode
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context)!.cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelSearch,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: Scaffold.of(context).openDrawer,
|
||||
icon: Icon(
|
||||
Icons.menu,
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: controller.isSearchMode
|
||||
? [
|
||||
if (controller.isSearching)
|
||||
const CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.setServer,
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
child: Text(
|
||||
controller.searchServer ??
|
||||
Matrix.of(context)
|
||||
.client
|
||||
.homeserver!
|
||||
.host,
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
),
|
||||
tooltip: L10n.of(context)!.addToStory,
|
||||
onPressed: () =>
|
||||
VRouter.of(context).to('/stories/create'),
|
||||
),
|
||||
ClientChooserButton(controller),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: selectMode == SelectMode.share
|
||||
? null
|
||||
: selectMode == SelectMode.select
|
||||
@ -75,138 +140,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
onPressed: controller.archiveAction,
|
||||
),
|
||||
]
|
||||
: [
|
||||
KeyBoardShortcuts(
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.keyF
|
||||
},
|
||||
onKeysPressed: () => VRouter.of(context).to('/search'),
|
||||
helpLabel: L10n.of(context)!.search,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
tooltip: L10n.of(context)!.search,
|
||||
onPressed: () => VRouter.of(context).to('/search'),
|
||||
),
|
||||
),
|
||||
if (selectMode == SelectMode.normal)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.camera_alt_outlined),
|
||||
tooltip: L10n.of(context)!.addToStory,
|
||||
onPressed: () =>
|
||||
VRouter.of(context).to('/stories/create'),
|
||||
),
|
||||
PopupMenuButton<PopupMenuAction>(
|
||||
onSelected: controller.onPopupMenuSelect,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.setStatus,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.edit_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.setStatus),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.newGroup,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.group_add_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.createNewGroup),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.newSpace,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.group_work_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.createNewSpace),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.adaptive.share_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.inviteContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.archive,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: PopupMenuAction.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
title: PageTransitionSwitcher(
|
||||
reverse: false,
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
layoutBuilder: (children) => Stack(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
children: children,
|
||||
),
|
||||
child: selectMode == SelectMode.share
|
||||
? Text(
|
||||
L10n.of(context)!.share,
|
||||
key: const ValueKey(SelectMode.share),
|
||||
)
|
||||
: selectMode == SelectMode.select
|
||||
? Text(
|
||||
controller.selectedRoomIds.length.toString(),
|
||||
key: const ValueKey(SelectMode.select),
|
||||
)
|
||||
: (() {
|
||||
final name = controller.activeSpaceId == null
|
||||
? AppConfig.applicationName
|
||||
: Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(controller.activeSpaceId!)!
|
||||
.displayname;
|
||||
return Text(name, key: ValueKey(name));
|
||||
})(),
|
||||
),
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,10 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:snapping_sheet/snapping_sheet.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_drawer.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import 'chat_list_body.dart';
|
||||
import 'chat_list_header.dart';
|
||||
@ -25,8 +23,6 @@ class ChatListView extends StatelessWidget {
|
||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
||||
builder: (_, __) {
|
||||
final selectMode = controller.selectMode;
|
||||
final showSpaces = controller.spacesEntries.length > 1 &&
|
||||
controller.selectedRoomIds.isEmpty;
|
||||
return VWidgetGuard(
|
||||
onSystemPop: (redirector) async {
|
||||
final selMode = controller.selectMode;
|
||||
@ -35,87 +31,28 @@ class ChatListView extends StatelessWidget {
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: ChatListHeader(controller: controller),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, size) {
|
||||
controller.snappingSheetContainerSize = size;
|
||||
return SnappingSheet(
|
||||
key: ValueKey(Matrix.of(context).client.userID.toString() +
|
||||
showSpaces.toString()),
|
||||
controller: controller.snappingSheetController,
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
height: controller.showChatBackupBanner ? 54 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
curve: Curves.bounceInOut,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListTile(
|
||||
leading: Image.asset(
|
||||
'assets/backup.png',
|
||||
fit: BoxFit.contain,
|
||||
width: 44,
|
||||
),
|
||||
title: Text(L10n.of(context)!.setupChatBackupNow),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: controller.firstRunBootstrapAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: ChatListViewBody(controller)),
|
||||
],
|
||||
),
|
||||
initialSnappingPosition: showSpaces
|
||||
? const SnappingPosition.pixels(
|
||||
positionPixels: kSpacesBottomBarHeight)
|
||||
: const SnappingPosition.factor(positionFactor: 0.0),
|
||||
snappingPositions: showSpaces
|
||||
? const [
|
||||
SnappingPosition.pixels(
|
||||
positionPixels: kSpacesBottomBarHeight),
|
||||
SnappingPosition.factor(positionFactor: 0.5),
|
||||
SnappingPosition.factor(positionFactor: 0.9),
|
||||
]
|
||||
: [const SnappingPosition.factor(positionFactor: 0.0)],
|
||||
sheetBelow: showSpaces
|
||||
? SnappingSheetContent(
|
||||
childScrollController:
|
||||
controller.snappingSheetScrollContentController,
|
||||
draggable: true,
|
||||
child: SpacesBottomBar(controller),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
body: ChatListViewBody(controller),
|
||||
drawer: ChatListDrawer(controller),
|
||||
floatingActionButton: selectMode == SelectMode.normal
|
||||
? Padding(
|
||||
padding: showSpaces
|
||||
? const EdgeInsets.only(bottom: 64.0)
|
||||
: const EdgeInsets.all(0),
|
||||
child: KeyBoardShortcuts(
|
||||
child: FloatingActionButton.extended(
|
||||
isExtended: controller.scrolledToTop,
|
||||
onPressed: () =>
|
||||
VRouter.of(context).to('/newprivatechat'),
|
||||
icon: const Icon(CupertinoIcons.chat_bubble),
|
||||
label: Text(L10n.of(context)!.newChat),
|
||||
),
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.keyN
|
||||
},
|
||||
onKeysPressed: () =>
|
||||
? KeyBoardShortcuts(
|
||||
child: FloatingActionButton.extended(
|
||||
isExtended: controller.scrolledToTop,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () =>
|
||||
VRouter.of(context).to('/newprivatechat'),
|
||||
helpLabel: L10n.of(context)!.newChat,
|
||||
icon: const Icon(CupertinoIcons.chat_bubble),
|
||||
label: Text(L10n.of(context)!.newChat),
|
||||
),
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.keyN
|
||||
},
|
||||
onKeysPressed: () =>
|
||||
VRouter.of(context).to('/newprivatechat'),
|
||||
helpLabel: L10n.of(context)!.newChat,
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: const SafeArea(
|
||||
child: ConnectionStatusHeader(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -79,6 +79,16 @@ class ClientChooserButton extends StatelessWidget {
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
PopupMenuItem(
|
||||
value: AddAccountAction.addAccount,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person_add_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context)!.addAccount),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@ -124,7 +134,8 @@ class ClientChooserButton extends StatelessWidget {
|
||||
),
|
||||
PopupMenuButton<Object>(
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.zero,
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
child: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name: snapshot.data?.displayName ??
|
||||
@ -158,6 +169,8 @@ class ClientChooserButton extends StatelessWidget {
|
||||
controller.setActiveClient(object);
|
||||
} else if (object is String) {
|
||||
controller.setActiveBundle(object);
|
||||
} else if (object == AddAccountAction.addAccount) {
|
||||
controller.addAccountAction();
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,3 +235,5 @@ class ClientChooserButton extends StatelessWidget {
|
||||
_handleKeyboardShortcut(matrix, lastIndex! - 1);
|
||||
}
|
||||
}
|
||||
|
||||
enum AddAccountAction { addAccount }
|
||||
|
@ -1,164 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:salomon_bottom_bar/salomon_bottom_bar.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_drawer.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
const kSpacesBottomBarHeight = 56.0;
|
||||
|
||||
final GlobalKey _globalKey = GlobalKey();
|
||||
|
||||
class SpacesBottomBar extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const SpacesBottomBar(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).navigationBarTheme.backgroundColor,
|
||||
elevation: 6,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppConfig.borderRadius)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SafeArea(
|
||||
child: StreamBuilder<Object>(
|
||||
stream: Matrix.of(context).client.onSync.stream.where((sync) =>
|
||||
(sync.rooms?.join?.values.any((r) =>
|
||||
r.state?.any((s) => s.type.startsWith('m.space')) ??
|
||||
false) ??
|
||||
false) ||
|
||||
(sync.rooms?.leave?.isNotEmpty ?? false)),
|
||||
builder: (context, snapshot) {
|
||||
return SingleChildScrollView(
|
||||
controller: controller.snappingSheetScrollContentController,
|
||||
child: AnimatedBuilder(
|
||||
child: _SpacesBottomNavigation(
|
||||
key: _globalKey, controller: controller),
|
||||
builder: (context, child) {
|
||||
if (controller.snappingSheetContainerSize == null) {
|
||||
return child!;
|
||||
}
|
||||
final rawPosition =
|
||||
controller.snappingSheetController.isAttached
|
||||
? controller.snappingSheetController.currentPosition
|
||||
: 0;
|
||||
final position = rawPosition /
|
||||
controller.snappingSheetContainerSize!.maxHeight;
|
||||
|
||||
if (rawPosition <= kSpacesBottomBarHeight) {
|
||||
return child!;
|
||||
} else if (position >= 0.5) {
|
||||
return SpacesDrawer(controller: controller);
|
||||
} else {
|
||||
final normalized = (rawPosition - kSpacesBottomBarHeight) /
|
||||
(controller.snappingSheetContainerSize!.maxHeight -
|
||||
kSpacesBottomBarHeight) *
|
||||
2;
|
||||
var boxHeight = (1 - normalized) * kSpacesBottomBarHeight;
|
||||
if (boxHeight < 0) boxHeight = 0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: boxHeight,
|
||||
child: ClipRect(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Opacity(
|
||||
opacity: 1 - normalized, child: child!)),
|
||||
),
|
||||
Opacity(
|
||||
opacity: normalized,
|
||||
child: SpacesDrawer(controller: controller),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
animation: controller.snappingSheetController,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SpacesBottomNavigation extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const _SpacesBottomNavigation({Key? key, required this.controller})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentIndex = controller.spacesEntries.indexWhere((space) =>
|
||||
controller.activeSpacesEntry.runtimeType == space.runtimeType &&
|
||||
(controller.activeSpaceId == space.getSpace(context)?.id)) +
|
||||
1;
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SalomonBottomBar(
|
||||
itemPadding: const EdgeInsets.all(8),
|
||||
currentIndex: currentIndex,
|
||||
onTap: (i) => i == 0
|
||||
? controller.expandSpaces()
|
||||
: controller.setActiveSpacesEntry(
|
||||
context,
|
||||
controller.spacesEntries[i - 1],
|
||||
),
|
||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
||||
items: [
|
||||
SalomonBottomBarItem(
|
||||
icon: const Icon(Icons.keyboard_arrow_up),
|
||||
title: Text(L10n.of(context)!.showSpaces),
|
||||
),
|
||||
...controller.spacesEntries
|
||||
.map((space) => _buildSpacesEntryUI(context, space))
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SalomonBottomBarItem _buildSpacesEntryUI(
|
||||
BuildContext context, SpacesEntry entry) {
|
||||
final space = entry.getSpace(context);
|
||||
if (space != null) {
|
||||
return SalomonBottomBarItem(
|
||||
icon: InkWell(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
onTap: () => controller.setActiveSpacesEntry(
|
||||
context,
|
||||
entry,
|
||||
),
|
||||
onLongPress: () => controller.editSpace(context, space.id),
|
||||
child: Avatar(
|
||||
mxContent: space.avatar,
|
||||
name: space.displayname,
|
||||
size: 24,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
title: Text(entry.getName(context)),
|
||||
);
|
||||
}
|
||||
return SalomonBottomBarItem(
|
||||
icon: entry.getIcon(false),
|
||||
activeIcon: entry.getIcon(true),
|
||||
title: Text(entry.getName(context)),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
@ -22,53 +23,93 @@ class SpacesDrawer extends StatelessWidget {
|
||||
|
||||
// TODO(TheOeWithTheBraid): wait for space hierarchy https://gitlab.com/famedly/company/frontend/libraries/matrix_api_lite/-/merge_requests/58
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
controller.snapBackSpacesSheet();
|
||||
return false;
|
||||
},
|
||||
child: Column(
|
||||
children: List.generate(spaceHierarchy.length, (index) {
|
||||
final space = spaceHierarchy.keys.toList()[index];
|
||||
final room = space.getSpace(context);
|
||||
final active = currentIndex == index;
|
||||
return ListView.builder(
|
||||
itemCount: spaceHierarchy.length + 2,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == spaceHierarchy.length) {
|
||||
return ListTile(
|
||||
selected: active,
|
||||
leading: index == 0
|
||||
? const Icon(Icons.keyboard_arrow_down)
|
||||
: room == null
|
||||
? space.getIcon(active)
|
||||
: Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: space.getName(context),
|
||||
size: 24,
|
||||
fontSize: 12,
|
||||
),
|
||||
title: Text(space.getName(context)),
|
||||
subtitle: room?.topic.isEmpty ?? true
|
||||
? null
|
||||
: Tooltip(
|
||||
message: room!.topic,
|
||||
child: Text(
|
||||
room.topic.replaceAll('\n', ' '),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
onTap: () => controller.setActiveSpacesEntry(
|
||||
context,
|
||||
space,
|
||||
leading: CircleAvatar(
|
||||
radius: Avatar.defaultSize / 2,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
child: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
),
|
||||
trailing: room != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
title: Text(L10n.of(context)!.archive),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
VRouter.of(context).to('/archive');
|
||||
},
|
||||
);
|
||||
}
|
||||
if (i == spaceHierarchy.length + 1) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
child: const Icon(Icons.group_work_outlined),
|
||||
radius: Avatar.defaultSize / 2,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
title: Text(L10n.of(context)!.createNewSpace),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
VRouter.of(context).to('/newspace');
|
||||
},
|
||||
);
|
||||
}
|
||||
final space = spaceHierarchy.keys.toList()[i];
|
||||
final room = space.getSpace(context);
|
||||
final active = currentIndex == i;
|
||||
return ListTile(
|
||||
selected: active,
|
||||
leading: room == null
|
||||
? CircleAvatar(
|
||||
child: space.getIcon(active),
|
||||
radius: Avatar.defaultSize / 2,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
)
|
||||
: Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: space.getName(context),
|
||||
),
|
||||
title: Text(
|
||||
space.getName(context),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: room?.topic.isEmpty ?? true
|
||||
? null
|
||||
: Tooltip(
|
||||
message: room!.topic,
|
||||
child: Text(
|
||||
room.topic.replaceAll('\n', ' '),
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
onTap: () => controller.setActiveSpacesEntry(
|
||||
context,
|
||||
space,
|
||||
),
|
||||
trailing: room != null
|
||||
? SizedBox(
|
||||
width: 32,
|
||||
child: IconButton(
|
||||
splashRadius: 24,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: L10n.of(context)!.edit,
|
||||
onPressed: () => controller.editSpace(context, room.id),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ enum ContextualRoomAction {
|
||||
}
|
||||
|
||||
class StoriesHeader extends StatelessWidget {
|
||||
const StoriesHeader({Key? key}) : super(key: key);
|
||||
final String filter;
|
||||
const StoriesHeader({required this.filter, Key? key}) : super(key: key);
|
||||
|
||||
void _addToStoryAction(BuildContext context) =>
|
||||
VRouter.of(context).to('/stories/create');
|
||||
@ -105,7 +106,10 @@ class StoriesHeader extends StatelessWidget {
|
||||
onTap: () => _addToStoryAction(context),
|
||||
);
|
||||
}
|
||||
if (client.storiesRooms.isEmpty) {
|
||||
if (client.storiesRooms.isEmpty ||
|
||||
!client.storiesRooms.any((room) => room.displayname
|
||||
.toLowerCase()
|
||||
.contains(filter.toLowerCase()))) {
|
||||
return Container();
|
||||
}
|
||||
final ownStoryRoom = client.storiesRooms
|
||||
@ -130,6 +134,11 @@ class StoriesHeader extends StatelessWidget {
|
||||
userId?.localpart ??
|
||||
'Unknown';
|
||||
final avatarUrl = snapshot.data?.avatarUrl;
|
||||
if (!displayname
|
||||
.toLowerCase()
|
||||
.contains(filter.toLowerCase())) {
|
||||
return Container();
|
||||
}
|
||||
return _StoryButton(
|
||||
profile: Profile(
|
||||
displayName: displayname,
|
||||
@ -139,7 +148,7 @@ class StoriesHeader extends StatelessWidget {
|
||||
hasPosts: room.hasPosts || room == ownStoryRoom,
|
||||
showEditFab: userId == client.userID,
|
||||
unread: room.membership == Membership.invite ||
|
||||
room.hasNewMessages,
|
||||
(room.hasNewMessages && room.hasPosts),
|
||||
onPressed: () => _goToStoryAction(context, room.id),
|
||||
onLongPressed: () =>
|
||||
_contextualActions(context, room),
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/connect/connect_page.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
@ -90,14 +89,14 @@ class ConnectPageView extends StatelessWidget {
|
||||
child: TextField(
|
||||
controller: controller.usernameController,
|
||||
onSubmitted: (_) => controller.signUp(),
|
||||
style: FluffyThemes.loginTextFieldStyle,
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
prefixIcon: const Icon(
|
||||
Icons.account_box_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
||||
hintText: L10n.of(context)!.chooseAUsername,
|
||||
errorText: controller.signupError,
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -106,12 +105,7 @@ class ConnectPageView extends StatelessWidget {
|
||||
child: Hero(
|
||||
tag: 'loginButton',
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.loading ? null : controller.signUp,
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(200),
|
||||
onPrimary: Colors.black,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
onPressed: controller.loading ? () {} : controller.signUp,
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.signUp),
|
||||
@ -148,11 +142,6 @@ class ConnectPageView extends StatelessWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller
|
||||
.ssoLoginAction(identityProviders.single.id!),
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(200),
|
||||
onPrimary: Colors.black,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
child: Text(identityProviders.single.name ??
|
||||
identityProviders.single.brand ??
|
||||
L10n.of(context)!.loginWithOneClick),
|
||||
@ -176,11 +165,6 @@ class ConnectPageView extends StatelessWidget {
|
||||
tag: 'signinButton',
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.loading ? () {} : controller.login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(200),
|
||||
onPrimary: Colors.black,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
child: Text(L10n.of(context)!.login),
|
||||
),
|
||||
),
|
||||
|
@ -22,17 +22,17 @@ class SsoButton extends StatelessWidget {
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 6.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: identityProvider.icon == null
|
||||
? const Icon(Icons.web_outlined)
|
||||
: CachedNetworkImage(
|
||||
|
@ -37,60 +37,53 @@ class DevicesSettingsView extends StatelessWidget {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2));
|
||||
}
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (controller.thisDevice != null)
|
||||
UserDeviceListItem(
|
||||
controller.thisDevice!,
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (controller.notThisDevice.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
controller.errorDeletingDevices ??
|
||||
L10n.of(context)!.removeAllOtherDevices,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
trailing: controller.loadingDeletingDevices
|
||||
? const CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2)
|
||||
: const Icon(Icons.delete_outline),
|
||||
onTap: controller.loadingDeletingDevices
|
||||
? null
|
||||
: () => controller
|
||||
.removeDevicesAction(controller.notThisDevice),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: controller.notThisDevice.isEmpty
|
||||
? Center(
|
||||
child: Icon(
|
||||
Icons.devices_other,
|
||||
size: 60,
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (BuildContext context, int i) =>
|
||||
const Divider(height: 1),
|
||||
itemCount: controller.notThisDevice.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
UserDeviceListItem(
|
||||
controller.notThisDevice[i],
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
return ListView.builder(
|
||||
itemCount: controller.notThisDevice.length + 1,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == 0) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (controller.thisDevice != null)
|
||||
UserDeviceListItem(
|
||||
controller.thisDevice!,
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(height: 1),
|
||||
if (controller.notThisDevice.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
controller.errorDeletingDevices ??
|
||||
L10n.of(context)!.removeAllOtherDevices,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
trailing: controller.loadingDeletingDevices
|
||||
? const CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2)
|
||||
: const Icon(Icons.delete_outline),
|
||||
onTap: controller.loadingDeletingDevices
|
||||
? null
|
||||
: () => controller.removeDevicesAction(
|
||||
controller.notThisDevice),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
i--;
|
||||
return UserDeviceListItem(
|
||||
controller.notThisDevice[i],
|
||||
rename: controller.renameDeviceAction,
|
||||
remove: (d) => controller.removeDevicesAction([d]),
|
||||
verify: controller.verifyDeviceAction,
|
||||
block: controller.blockDeviceAction,
|
||||
unblock: controller.unblockDeviceAction,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -108,12 +108,13 @@ class UserDeviceListItem extends StatelessWidget {
|
||||
),
|
||||
title: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
userDevice.displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Expanded(
|
||||
child: Text(
|
||||
userDevice.displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (keys != null)
|
||||
Text(
|
||||
keys.blocked
|
||||
|
@ -5,7 +5,6 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'homeserver_picker.dart';
|
||||
@ -22,131 +21,127 @@ class HomeserverPickerView extends StatelessWidget {
|
||||
appBar: VRouter.of(context).path == '/home'
|
||||
? null
|
||||
: AppBar(title: Text(L10n.of(context)!.addAccount)),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: controller.displayServerList ? 0 : 256),
|
||||
alignment: Alignment.center,
|
||||
child: Image.asset('assets/info-logo.png'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
focusNode: controller.homeserverFocusNode,
|
||||
controller: controller.homeserverController,
|
||||
onChanged: controller.onChanged,
|
||||
style: FluffyThemes.loginTextFieldStyle,
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
labelText: L10n.of(context)!.homeserver,
|
||||
hintText: L10n.of(context)!.enterYourHomeserver,
|
||||
suffixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Colors.black,
|
||||
),
|
||||
errorText: controller.error,
|
||||
),
|
||||
readOnly: !AppConfig.allowOtherHomeservers,
|
||||
onSubmitted: (_) => controller.checkHomeserverAction(),
|
||||
autocorrect: false,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
height: 256,
|
||||
child: Image.asset('assets/info-logo.png'),
|
||||
),
|
||||
),
|
||||
if (controller.displayServerList)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Material(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Colors.white.withAlpha(200),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: benchmarkResults == null
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
))
|
||||
: Column(
|
||||
children: controller.filteredHomeservers
|
||||
.map(
|
||||
(server) => ListTile(
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.info_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () =>
|
||||
controller.showServerInfo(server),
|
||||
),
|
||||
onTap: () => controller.setServer(
|
||||
server.homeserver.baseUrl.host),
|
||||
title: Text(
|
||||
server.homeserver.baseUrl.host,
|
||||
style: const TextStyle(
|
||||
color: Colors.black),
|
||||
),
|
||||
subtitle: Text(
|
||||
server.homeserver.description ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
child: TextField(
|
||||
focusNode: controller.homeserverFocusNode,
|
||||
controller: controller.homeserverController,
|
||||
onChanged: controller.onChanged,
|
||||
decoration: InputDecoration(
|
||||
prefixText: '${L10n.of(context)!.homeserver}: ',
|
||||
hintText: L10n.of(context)!.enterYourHomeserver,
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
errorText: controller.error,
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
readOnly: !AppConfig.allowOtherHomeservers,
|
||||
onSubmitted: (_) => controller.checkHomeserverAction(),
|
||||
autocorrect: false,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => launch(AppConfig.privacyUrl),
|
||||
child: Text(
|
||||
L10n.of(context)!.privacy,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
),
|
||||
if (controller.displayServerList)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Material(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Colors.white.withAlpha(200),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: benchmarkResults == null
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
))
|
||||
: Column(
|
||||
children: controller.filteredHomeservers
|
||||
.map(
|
||||
(server) => ListTile(
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.info_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
onPressed: () =>
|
||||
controller.showServerInfo(server),
|
||||
),
|
||||
onTap: () => controller.setServer(
|
||||
server.homeserver.baseUrl.host),
|
||||
title: Text(
|
||||
server.homeserver.baseUrl.host,
|
||||
style: const TextStyle(
|
||||
color: Colors.black),
|
||||
),
|
||||
subtitle: Text(
|
||||
server.homeserver.description ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => PlatformInfos.showDialog(context),
|
||||
child: Text(
|
||||
L10n.of(context)!.about,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => launch(AppConfig.privacyUrl),
|
||||
child: Text(
|
||||
L10n.of(context)!.privacy,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Hero(
|
||||
tag: 'loginButton',
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading
|
||||
? () {}
|
||||
: controller.checkHomeserverAction,
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(200),
|
||||
onPrimary: Colors.black,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
child: controller.isLoading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.connect),
|
||||
TextButton(
|
||||
onPressed: () => PlatformInfos.showDialog(context),
|
||||
child: Text(
|
||||
L10n.of(context)!.about,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: double.infinity,
|
||||
child: Hero(
|
||||
tag: 'loginButton',
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: controller.checkHomeserverAction,
|
||||
child: controller.isLoading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.connect),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'login.dart';
|
||||
@ -44,16 +43,16 @@ class LoginView extends StatelessWidget {
|
||||
controller: controller.usernameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: FluffyThemes.loginTextFieldStyle,
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.username],
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
prefixIcon: const Icon(
|
||||
Icons.account_box_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.account_box_outlined),
|
||||
errorText: controller.usernameError,
|
||||
hintText: L10n.of(context)!.emailOrUsername,
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -68,12 +67,8 @@ class LoginView extends StatelessWidget {
|
||||
textInputAction: TextInputAction.next,
|
||||
obscureText: !controller.showPassword,
|
||||
onSubmitted: controller.login,
|
||||
style: FluffyThemes.loginTextFieldStyle,
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.lock_outlined),
|
||||
errorText: controller.passwordError,
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context)!.showPassword,
|
||||
@ -86,6 +81,10 @@ class LoginView extends StatelessWidget {
|
||||
onPressed: controller.toggleShowPassword,
|
||||
),
|
||||
hintText: L10n.of(context)!.password,
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -97,11 +96,6 @@ class LoginView extends StatelessWidget {
|
||||
onPressed: controller.loading
|
||||
? null
|
||||
: () => controller.login(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(200),
|
||||
onPrimary: Colors.black,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.login),
|
||||
@ -126,11 +120,7 @@ class LoginView extends StatelessWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.loading ? () {} : controller.passwordForgotten,
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(156),
|
||||
onPrimary: Colors.red,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(onPrimary: Colors.red),
|
||||
child: Text(L10n.of(context)!.passwordForgotten),
|
||||
),
|
||||
),
|
||||
|
@ -1,125 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/utils/famedlysdk_store.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
|
||||
import 'search_view.dart';
|
||||
|
||||
class Search extends StatefulWidget {
|
||||
const Search({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
SearchController createState() => SearchController();
|
||||
}
|
||||
|
||||
class SearchController extends State<Search> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
Future<QueryPublicRoomsResponse>? publicRoomsResponse;
|
||||
String? lastServer;
|
||||
Timer? _coolDown;
|
||||
String? genericSearchTerm;
|
||||
|
||||
void search(String query) async {
|
||||
setState(() {});
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(
|
||||
const Duration(milliseconds: 500),
|
||||
() => setState(() {
|
||||
genericSearchTerm = query;
|
||||
publicRoomsResponse = null;
|
||||
searchUser(context, controller.text);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void joinGroupAction(PublicRoomsChunk room) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (c) => PublicRoomBottomSheet(
|
||||
roomAlias: room.canonicalAlias ?? room.roomId,
|
||||
outerContext: context,
|
||||
chunk: room,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? server;
|
||||
|
||||
static const String _serverStoreNamespace = 'im.fluffychat.search.server';
|
||||
|
||||
void setServer() async {
|
||||
final newServer = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
title: L10n.of(context)!.changeTheHomeserver,
|
||||
context: context,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
prefixText: 'https://',
|
||||
hintText: Matrix.of(context).client.homeserver?.host,
|
||||
initialText: server,
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false)
|
||||
]);
|
||||
if (newServer == null) return;
|
||||
Store().setItem(_serverStoreNamespace, newServer.single);
|
||||
setState(() {
|
||||
server = newServer.single;
|
||||
});
|
||||
}
|
||||
|
||||
String? currentSearchTerm;
|
||||
List<Profile> foundProfiles = [];
|
||||
|
||||
static const searchUserDirectoryLimit = 10;
|
||||
|
||||
void searchUser(BuildContext context, String text) async {
|
||||
if (text.isEmpty) {
|
||||
setState(() {
|
||||
foundProfiles = [];
|
||||
});
|
||||
}
|
||||
currentSearchTerm = text;
|
||||
if (currentSearchTerm?.isEmpty ?? true) return;
|
||||
final matrix = Matrix.of(context);
|
||||
SearchUserDirectoryResponse? response;
|
||||
try {
|
||||
response = await matrix.client.searchUserDirectory(
|
||||
text,
|
||||
limit: searchUserDirectoryLimit,
|
||||
);
|
||||
} catch (_) {}
|
||||
foundProfiles = List<Profile>.from(response?.results ?? []);
|
||||
if (foundProfiles.isEmpty && text.isValidMatrixId && text.sigil == '@') {
|
||||
foundProfiles.add(Profile.fromJson({
|
||||
'displayname': text.localpart,
|
||||
'user_id': text,
|
||||
}));
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
controller.text = VRouter.of(context).queryParameters['query'] ?? '';
|
||||
final server = await Store().getItem(_serverStoreNamespace);
|
||||
if (server?.isNotEmpty ?? false) {
|
||||
this.server = server;
|
||||
}
|
||||
search(controller.text);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SearchView(this);
|
||||
}
|
@ -1,299 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/utils/string_extension.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/contacts_list.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../utils/platform_infos.dart';
|
||||
import 'search.dart';
|
||||
|
||||
class SearchView extends StatelessWidget {
|
||||
final SearchController controller;
|
||||
|
||||
const SearchView(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final server = controller.genericSearchTerm?.isValidMatrixId ?? false
|
||||
? controller.genericSearchTerm!.domain
|
||||
: controller.server;
|
||||
if (controller.lastServer != server) {
|
||||
controller.lastServer = server;
|
||||
controller.publicRoomsResponse = null;
|
||||
}
|
||||
controller.publicRoomsResponse ??= Matrix.of(context)
|
||||
.client
|
||||
.queryPublicRooms(
|
||||
server: server,
|
||||
filter: PublicRoomQueryFilter(
|
||||
genericSearchTerm: controller.genericSearchTerm,
|
||||
),
|
||||
)
|
||||
.catchError((error) {
|
||||
if (!(controller.genericSearchTerm?.isValidMatrixId ?? false)) {
|
||||
throw error;
|
||||
}
|
||||
return QueryPublicRoomsResponse.fromJson({
|
||||
'chunk': [],
|
||||
});
|
||||
}).then((QueryPublicRoomsResponse res) {
|
||||
final genericSearchTerm = controller.genericSearchTerm;
|
||||
if (genericSearchTerm != null &&
|
||||
!res.chunk.any(
|
||||
(room) => room.canonicalAlias == controller.genericSearchTerm)) {
|
||||
// we have to tack on the original alias
|
||||
res.chunk.add(
|
||||
PublicRoomsChunk(
|
||||
name: genericSearchTerm,
|
||||
numJoinedMembers: 0,
|
||||
roomId: '!unknown',
|
||||
worldReadable: true,
|
||||
guestCanJoin: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
final rooms = List<Room>.from(Matrix.of(context).client.rooms);
|
||||
rooms.removeWhere(
|
||||
(room) =>
|
||||
room.lastEvent == null ||
|
||||
!room.displayname.toLowerCase().removeDiacritics().contains(
|
||||
controller.controller.text.toLowerCase().removeDiacritics()),
|
||||
);
|
||||
const tabCount = 3;
|
||||
return DefaultTabController(
|
||||
length: tabCount,
|
||||
initialIndex: controller.controller.text.startsWith('#') ? 0 : 1,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
titleSpacing: 0,
|
||||
title: TextField(
|
||||
autofocus: true,
|
||||
controller: controller.controller,
|
||||
decoration: InputDecoration(
|
||||
suffix: const Icon(Icons.search_outlined),
|
||||
hintText: L10n.of(context)!.search,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
onChanged: controller.search,
|
||||
),
|
||||
bottom: TabBar(
|
||||
indicatorColor: Theme.of(context).colorScheme.secondary,
|
||||
labelColor: Theme.of(context).colorScheme.secondary,
|
||||
unselectedLabelColor: Theme.of(context).textTheme.bodyText1!.color,
|
||||
labelStyle: const TextStyle(fontSize: 16),
|
||||
labelPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 0,
|
||||
),
|
||||
tabs: [
|
||||
Tab(child: Text(L10n.of(context)!.discover, maxLines: 1)),
|
||||
Tab(child: Text(L10n.of(context)!.chats, maxLines: 1)),
|
||||
Tab(child: Text(L10n.of(context)!.people, maxLines: 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
ListView(
|
||||
keyboardDismissBehavior: PlatformInfos.isIOS
|
||||
? ScrollViewKeyboardDismissBehavior.onDrag
|
||||
: ScrollViewKeyboardDismissBehavior.manual,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Theme.of(context).colorScheme.secondary,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
child: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)!.changeTheServer),
|
||||
onTap: controller.setServer,
|
||||
),
|
||||
FutureBuilder<QueryPublicRoomsResponse>(
|
||||
future: controller.publicRoomsResponse,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<QueryPublicRoomsResponse> snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
const Icon(
|
||||
Icons.error_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
snapshot.error!.toLocalizedString(context),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
}
|
||||
final publicRoomsResponse = snapshot.data!;
|
||||
if (publicRoomsResponse.chunk.isEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
const Icon(
|
||||
Icons.search_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.noPublicRoomsFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(12),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: publicRoomsResponse.chunk.length,
|
||||
itemBuilder: (BuildContext context, int i) => Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: () => controller.joinGroupAction(
|
||||
publicRoomsResponse.chunk[i],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent:
|
||||
publicRoomsResponse.chunk[i].avatarUrl,
|
||||
name: publicRoomsResponse.chunk[i].name,
|
||||
),
|
||||
Text(
|
||||
publicRoomsResponse.chunk[i].name!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context)!.countParticipants(
|
||||
publicRoomsResponse
|
||||
.chunk[i].numJoinedMembers),
|
||||
style: const TextStyle(fontSize: 10.5),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
publicRoomsResponse.chunk[i].topic ??
|
||||
L10n.of(context)!.noDescription,
|
||||
maxLines: 4,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
ListView.builder(
|
||||
keyboardDismissBehavior: PlatformInfos.isIOS
|
||||
? ScrollViewKeyboardDismissBehavior.onDrag
|
||||
: ScrollViewKeyboardDismissBehavior.manual,
|
||||
itemCount: rooms.length,
|
||||
itemBuilder: (_, i) => ChatListItem(rooms[i]),
|
||||
),
|
||||
controller.foundProfiles.isNotEmpty
|
||||
? ListView.builder(
|
||||
keyboardDismissBehavior: PlatformInfos.isIOS
|
||||
? ScrollViewKeyboardDismissBehavior.onDrag
|
||||
: ScrollViewKeyboardDismissBehavior.manual,
|
||||
itemCount: controller.foundProfiles.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
final foundProfile = controller.foundProfiles[i];
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
final roomID = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final client = Matrix.of(context).client;
|
||||
final roomId = await client
|
||||
.startDirectChat(foundProfile.userId);
|
||||
return roomId;
|
||||
},
|
||||
);
|
||||
if (roomID.error == null) {
|
||||
VRouter.of(context)
|
||||
.toSegments(['rooms', roomID.result!]);
|
||||
}
|
||||
},
|
||||
leading: Avatar(
|
||||
mxContent: foundProfile.avatarUrl,
|
||||
name: foundProfile.displayName ?? foundProfile.userId,
|
||||
//size: 24,
|
||||
),
|
||||
title: Text(
|
||||
foundProfile.displayName ??
|
||||
foundProfile.userId.localpart!,
|
||||
style: const TextStyle(),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
foundProfile.userId,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: ContactsList(searchController: controller.controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ class SettingsView extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).textTheme.bodyText1!.color,
|
||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'signup.dart';
|
||||
|
||||
@ -38,11 +37,8 @@ class SignupPageView extends StatelessWidget {
|
||||
controller: controller.passwordController,
|
||||
obscureText: !controller.showPassword,
|
||||
validator: controller.password1TextFieldValidator,
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
prefixIcon: const Icon(
|
||||
Icons.vpn_key_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.vpn_key_outlined),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: L10n.of(context)!.showPassword,
|
||||
icon: Icon(
|
||||
@ -53,7 +49,12 @@ class SignupPageView extends StatelessWidget {
|
||||
),
|
||||
onPressed: controller.toggleShowPassword,
|
||||
),
|
||||
errorStyle: const TextStyle(color: Colors.orange),
|
||||
hintText: L10n.of(context)!.chooseAStrongPassword,
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -68,12 +69,14 @@ class SignupPageView extends StatelessWidget {
|
||||
controller: controller.password2Controller,
|
||||
obscureText: !controller.showPassword,
|
||||
validator: controller.password2TextFieldValidator,
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
prefixIcon: const Icon(
|
||||
Icons.repeat_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.repeat_outlined),
|
||||
hintText: L10n.of(context)!.repeatPassword,
|
||||
errorStyle: const TextStyle(color: Colors.orange),
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -87,16 +90,19 @@ class SignupPageView extends StatelessWidget {
|
||||
autofillHints:
|
||||
controller.loading ? null : [AutofillHints.username],
|
||||
validator: controller.emailTextFieldValidator,
|
||||
decoration: FluffyThemes.loginTextFieldDecoration(
|
||||
prefixIcon: const Icon(
|
||||
Icons.mail_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.mail_outlined),
|
||||
hintText: L10n.of(context)!.enterAnEmailAddress,
|
||||
errorText: controller.error,
|
||||
errorColor: controller.emailController.text.isEmpty
|
||||
? Colors.orangeAccent
|
||||
: null,
|
||||
fillColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.75),
|
||||
errorStyle: TextStyle(
|
||||
color: controller.emailController.text.isEmpty
|
||||
? Colors.orangeAccent
|
||||
: Colors.orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -106,11 +112,6 @@ class SignupPageView extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.loading ? () {} : controller.signup,
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.white.withAlpha(200),
|
||||
onPrimary: Colors.black,
|
||||
shadowColor: Colors.white,
|
||||
),
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.signUp),
|
||||
|
@ -12,10 +12,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- emoji_picker_flutter (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_app_badger (1.3.0):
|
||||
- FlutterMacOS
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (3.3.1):
|
||||
@ -24,7 +20,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- flutter_webrtc (0.7.1):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 97.4692.02)
|
||||
- WebRTC-SDK (= 97.4692.05)
|
||||
- FlutterMacOS (1.0.0)
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
@ -33,13 +29,15 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- just_audio (0.0.1):
|
||||
- FlutterMacOS
|
||||
- package_info (0.0.1):
|
||||
- FlutterMacOS
|
||||
- package_info_plus_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- record_macos (1.0.0):
|
||||
- FlutterMacOS
|
||||
- share_plus_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.2):
|
||||
@ -51,7 +49,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- wakelock_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (97.4692.02)
|
||||
- WebRTC-SDK (97.4692.05)
|
||||
|
||||
DEPENDENCIES:
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||
@ -60,8 +58,6 @@ DEPENDENCIES:
|
||||
- desktop_lifecycle (from `Flutter/ephemeral/.symlinks/plugins/desktop_lifecycle/macos`)
|
||||
- device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`)
|
||||
- emoji_picker_flutter (from `Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||
- flutter_web_auth (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth/macos`)
|
||||
@ -69,9 +65,10 @@ DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`)
|
||||
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`)
|
||||
- package_info (from `Flutter/ephemeral/.symlinks/plugins/package_info/macos`)
|
||||
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||
- share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
|
||||
- shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
@ -97,10 +94,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos
|
||||
emoji_picker_flutter:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
flutter_app_badger:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos
|
||||
flutter_local_notifications:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||
flutter_secure_storage_macos:
|
||||
@ -115,12 +108,14 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
|
||||
just_audio:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos
|
||||
package_info:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info/macos
|
||||
package_info_plus_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
|
||||
path_provider_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
||||
record_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||
share_plus_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
|
||||
shared_preferences_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos
|
||||
sqflite:
|
||||
@ -139,26 +134,25 @@ SPEC CHECKSUMS:
|
||||
desktop_lifecycle: a600c10e12fe033c7be9078f2e929b8241f2c1e3
|
||||
device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7
|
||||
emoji_picker_flutter: 533634326b1c5de9a181ba14b9758e6dfe967a20
|
||||
file_selector_macos: ff6dc948d4ddd34e8602a1f60b7d0b4cc6051a47
|
||||
flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730
|
||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||
flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa
|
||||
flutter_web_auth: ae2c29ca9b98c00b4e0e8c0919bb4a05d44b76df
|
||||
flutter_webrtc: 238124d0a7ba1c43543791f31a92a672370497c2
|
||||
flutter_webrtc: 37c4efd66d9d306878c1323d5ac5a1d10c748b3a
|
||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
geolocator_apple: 821be05bbdb1b49500e029ebcbf2d6acf2dfb966
|
||||
just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489
|
||||
package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2
|
||||
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
|
||||
path_provider_macos: 160cab0d5461f0c0e02995469a98f24bdb9a3f1f
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
record_macos: dcf4f2bb654970437e012521cb4ea1fca4f78bb9
|
||||
share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
|
||||
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||
wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
|
||||
WebRTC-SDK: dda4e50186f9eed672dc6bcf4faafb30c6ce48e3
|
||||
WebRTC-SDK: a6ee40bda0e3f7dba057907c3897374005c5715b
|
||||
|
||||
PODFILE CHECKSUM: 9b8d08a513b178c33212d1b54cc9e3cba756d95b
|
||||
|
||||
|
@ -268,17 +268,16 @@
|
||||
"${BUILT_PRODUCTS_DIR}/desktop_lifecycle/desktop_lifecycle.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus_macos/device_info_plus_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/emoji_picker_flutter/emoji_picker_flutter.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage_macos/flutter_secure_storage_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_web_auth/flutter_web_auth.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_webrtc/flutter_webrtc.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/just_audio/just_audio.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/package_info/package_info.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/package_info_plus_macos/package_info_plus_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/record_macos/record_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/share_plus_macos/share_plus_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/shared_preferences_macos/shared_preferences_macos.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework",
|
||||
@ -296,17 +295,16 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/desktop_lifecycle.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/emoji_picker_flutter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_webrtc.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/just_audio.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/record_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_macos.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework",
|
||||
|
@ -1423,13 +1423,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.3"
|
||||
salomon_bottom_bar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: salomon_bottom_bar
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
scroll_to_index:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -71,7 +71,6 @@ dependencies:
|
||||
qr_flutter: ^4.0.0
|
||||
receive_sharing_intent: ^1.4.5
|
||||
record: ^4.1.1
|
||||
salomon_bottom_bar: ^3.2.0
|
||||
scroll_to_index: ^3.0.1
|
||||
sentry: ^6.3.0
|
||||
share_plus: ^4.0.9
|
||||
|
Loading…
Reference in New Issue
Block a user