Initial commit
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2026-09-11 15:59:56 -03:00
commit 0037b0b9cf
651 changed files with 199705 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'dart:async';
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/pages/invitation_selection/invitation_selection_view.dart';
import 'package:tatchat/widgets/future_loading_dialog.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import '../../utils/localized_exception_extension.dart';
class InvitationSelection extends StatefulWidget {
final String roomId;
const InvitationSelection({super.key, required this.roomId});
@override
InvitationSelectionController createState() =>
InvitationSelectionController();
}
class InvitationSelectionController extends State<InvitationSelection> {
TextEditingController controller = TextEditingController();
late String currentSearchTerm;
bool loading = false;
List<Profile> foundProfiles = [];
Timer? coolDown;
String? get roomId => widget.roomId;
Future<List<User>> getContacts(BuildContext context) async {
final client = Matrix.of(context).client;
final room = client.getRoomById(roomId!)!;
final participants = (room.summary.mJoinedMemberCount ?? 0) > 100
? room.getParticipants()
: await room.requestParticipants();
participants.removeWhere(
(u) => ![Membership.join, Membership.invite].contains(u.membership),
);
final contacts = client.rooms
.where((r) => r.isDirectChat)
.map((r) => r.unsafeGetUserFromMemoryOrFallback(r.directChatMatrixID!))
.toList();
contacts.sort(
(a, b) => a.calcDisplayname().toLowerCase().compareTo(
b.calcDisplayname().toLowerCase(),
),
);
return contacts;
}
Future<void> inviteAction(
BuildContext context,
String id,
String displayname,
) async {
final l10n = L10n.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final room = Matrix.of(context).client.getRoomById(roomId!)!;
final success = await showFutureLoadingDialog(
context: context,
future: () => room.invite(id),
);
if (success.error == null) {
if (!context.mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.contactHasBeenInvitedToTheGroup)),
);
}
}
void searchUserWithCoolDown(String text) {
coolDown?.cancel();
coolDown = Timer(
const Duration(milliseconds: 500),
() => searchUser(context, text),
);
}
Future<void> searchUser(BuildContext context, String text) async {
coolDown?.cancel();
if (text.isEmpty) {
setState(() => foundProfiles = []);
}
currentSearchTerm = text;
if (currentSearchTerm.isEmpty) return;
if (loading) return;
setState(() => loading = true);
final matrix = Matrix.of(context);
SearchUserDirectoryResponse response;
try {
response = await matrix.client.searchUserDirectory(text, limit: 10);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text((e).toLocalizedString(context))));
return;
} finally {
setState(() => loading = false);
}
setState(() {
foundProfiles = List<Profile>.from(response.results);
if (text.isValidMatrixIdStrict() &&
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
setState(
() => foundProfiles = [
Profile.fromJson({'user_id': text}),
],
);
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => InvitationSelectionView(this);
}

View File

@@ -0,0 +1,204 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:tatchat/l10n/l10n.dart';
import 'package:tatchat/pages/invitation_selection/invitation_selection.dart';
import 'package:tatchat/widgets/avatar.dart';
import 'package:tatchat/widgets/layouts/max_width_body.dart';
import 'package:tatchat/widgets/matrix.dart';
import 'package:material_ui/material_ui.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class InvitationSelectionView extends StatelessWidget {
final InvitationSelectionController controller;
const InvitationSelectionView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final room = Matrix.of(
context,
).client.getRoomById(controller.widget.roomId);
if (room == null) {
return Scaffold(
appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
),
);
}
final groupName = room.name.isEmpty ? L10n.of(context).group : room.name;
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
leading: const Center(child: BackButton()),
titleSpacing: 0,
title: Text(L10n.of(context).inviteContact),
),
body: MaxWidthBody(
innerPadding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
hintText: L10n.of(context).inviteContactToGroup(groupName),
prefixIcon: controller.loading
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
: const Icon(Icons.search_outlined),
),
onChanged: controller.searchUserWithCoolDown,
),
),
StreamBuilder<Object>(
stream: room.client.onRoomState.stream.where(
(update) => update.roomId == room.id,
),
builder: (context, snapshot) {
final participants = room
.getParticipants()
.map((user) => user.id)
.toSet();
return controller.foundProfiles.isNotEmpty
? ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: controller.foundProfiles.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
profile: controller.foundProfiles[i],
isMember: participants.contains(
controller.foundProfiles[i].userId,
),
onTap: () => controller.inviteAction(
context,
controller.foundProfiles[i].userId,
controller.foundProfiles[i].displayName ??
controller
.foundProfiles[i]
.userId
.localpart ??
L10n.of(context).user,
),
),
)
: FutureBuilder<List<User>>(
future: controller.getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
final contacts = snapshot.data!;
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: contacts.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
user: contacts[i],
profile: Profile(
avatarUrl: contacts[i].avatarUrl,
displayName:
contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
userId: contacts[i].id,
),
isMember: participants.contains(
contacts[i].id,
),
onTap: () => controller.inviteAction(
context,
contacts[i].id,
contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
),
),
);
},
);
},
),
],
),
),
);
}
}
class _InviteContactListTile extends StatelessWidget {
final Profile profile;
final User? user;
final bool isMember;
final void Function() onTap;
const _InviteContactListTile({
required this.profile,
this.user,
required this.isMember,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = L10n.of(context);
return ListTile(
leading: Avatar(
mxContent: profile.avatarUrl,
name: profile.displayName,
presenceUserId: profile.userId,
onTap: () => UserDialog.show(context: context, profile: profile),
),
title: Text(
profile.displayName ?? profile.userId.localpart ?? l10n.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
profile.userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: theme.colorScheme.secondary),
),
trailing: TextButton(
onPressed: isMember ? null : onTap,
child: Text(isMember ? l10n.participant : l10n.invite),
),
);
}
}