Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a0f0a9e
setup for task invitation
anisha-e10 Jun 2, 2025
e7179ef
Merge remote-tracking branch 'origin/main' into anisha/invite-task
anisha-e10 Jun 6, 2025
4120230
callback and providers added
anisha-e10 Jun 6, 2025
8d412f7
callback added
anisha-e10 Jun 6, 2025
28c7557
view invited users list
anisha-e10 Jun 6, 2025
cdd29d0
text changes
anisha-e10 Jun 9, 2025
64bd50a
Merge remote-tracking branch 'refs/remotes/origin/main' into anisha/i…
anisha-e10 Jun 11, 2025
cf5e8b4
notifiers added
anisha-e10 Jun 11, 2025
1fd04d2
managed UI
anisha-e10 Jun 11, 2025
d5e331e
managed actions
anisha-e10 Jun 11, 2025
acd609d
invite and invited actions added when user is invited for task
anisha-e10 Jun 11, 2025
38516d0
modified code structure of task assignment and task invitations
anisha-e10 Jun 11, 2025
177129e
assignment and invitations test cases
anisha-e10 Jun 13, 2025
91e0739
notifier test added for task and invitations
anisha-e10 Jun 16, 2025
a544d49
task item test resolved
anisha-e10 Jun 16, 2025
4713403
user builder test added
anisha-e10 Jun 16, 2025
bd70ce5
space detail page test cases solved
anisha-e10 Jun 16, 2025
75da519
lint error
anisha-e10 Jun 16, 2025
bce5fcf
fix invitation item widget test cases mock data
anisha-e10 Jun 16, 2025
6b3910a
solved mock data
anisha-e10 Jun 16, 2025
10d3cb9
invite actions test added
anisha-e10 Jun 16, 2025
437b908
Merge remote-tracking branch 'origin/main' into anisha/invite-task
anisha-e10 Jun 16, 2025
a97f607
Merge remote-tracking branch 'refs/remotes/origin/main' into anisha/i…
anisha-e10 Jun 25, 2025
3422798
Merge remote-tracking branch 'refs/remotes/origin/main' into anisha/i…
anisha-e10 Jul 8, 2025
a7b9f53
Added view and notifier to check the user is invited or provide accep…
anisha-e10 Jul 8, 2025
f431b9d
text changes
anisha-e10 Jul 8, 2025
c0c3104
assign and unassign test cases added
anisha-e10 Jul 9, 2025
b4e338e
test cases added that cover all line of code
anisha-e10 Jul 9, 2025
beabef5
added test for accept and decline view
anisha-e10 Jul 9, 2025
be39519
task assignment widget remaining test added
anisha-e10 Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/lib/common/toolkit/menu_item_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class MenuItemWidget extends StatelessWidget {
final disabledColor = Theme.of(context).disabledColor;
return Card(
child: ListTile(
contentPadding: EdgeInsets.zero,
key: innerKey,
onTap: onTap,
visualDensity: visualDensity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@

class InviteIndividualUsers extends ConsumerWidget {
final String roomId;
final Task? task;
final bool isFullPageMode;

const InviteIndividualUsers({super.key, required this.roomId, this.isFullPageMode = true});
const InviteIndividualUsers({
super.key,
required this.roomId,
this.isFullPageMode = true,
this.task,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
Expand Down Expand Up @@ -72,11 +78,12 @@
roomId: roomId,
userProfile: profile,
includeSharedRooms: isSuggestion,
task: task,

Check warning on line 81 in app/lib/features/invite_members/pages/invite_individual_users.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/invite_members/pages/invite_individual_users.dart#L81

Added line #L81 was not covered by tests
);
},
),
),
if (!isFullPageMode)...[
if (!isFullPageMode || task != null)...[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
Expand Down
18 changes: 17 additions & 1 deletion app/lib/features/invite_members/widgets/direct_invite.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:acter/common/providers/room_providers.dart';
import 'package:acter/features/member/actions/invite_actions.dart';
import 'package:acter/features/member/widgets/user_builder.dart';
import 'package:flutter/material.dart';
import 'package:acter/l10n/generated/l10n.dart';
Expand Down Expand Up @@ -26,7 +27,22 @@
: Text(userId),
trailing:
room != null
? UserStateButton(userId: userId, room: room)
? UserStateButton(
userId: userId,
room: room,
onInvite: (userId) => InviteActions.handleInvite(

Check warning on line 33 in app/lib/features/invite_members/widgets/direct_invite.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/invite_members/widgets/direct_invite.dart#L33

Added line #L33 was not covered by tests
context: context,
ref: ref,
userId: userId,
room: room,
),
onCancelInvite: (userId) => InviteActions.handleCancelInvite(

Check warning on line 39 in app/lib/features/invite_members/widgets/direct_invite.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/invite_members/widgets/direct_invite.dart#L39

Added line #L39 was not covered by tests
context: context,
ref: ref,
userId: userId,
room: room,
),
)
: const Skeletonizer(child: Text('Loading room')),
),
);
Expand Down
55 changes: 55 additions & 0 deletions app/lib/features/member/actions/invite_actions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:acter/common/providers/room_providers.dart';
import 'package:acter/l10n/generated/l10n.dart';
import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class InviteActions {
static Future<void> handleInvite({
required BuildContext context,
required WidgetRef ref,
required String userId,
required Room room,
Task? task,
}) async {
final lang = L10n.of(context);
EasyLoading.show(status: lang.invitingLoading(userId), dismissOnTap: false);
try {
if (task != null) {
final invitationsManager = await task.invitations();
await invitationsManager.invite(userId);
} else {
await room.inviteUser(userId);
}
EasyLoading.dismiss();
} catch (e) {
// ignore: use_build_context_synchronously
EasyLoading.showToast(lang.invitingError(e, userId));
}
}

static Future<void> handleCancelInvite({
required BuildContext context,
required WidgetRef ref,
required String userId,
required Room room,
}) async {
final lang = L10n.of(context);
EasyLoading.show(
status: lang.cancelInviteLoading(userId),
dismissOnTap: false,
);
try {
final member = ref.read(memberProvider((userId: userId, roomId: room.roomIdStr()))).valueOrNull;

if (member != null) {
await member.kick('Cancel Invite');

Check warning on line 47 in app/lib/features/member/actions/invite_actions.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/member/actions/invite_actions.dart#L47

Added line #L47 was not covered by tests
}
EasyLoading.dismiss();
} catch (e) {
// ignore: use_build_context_synchronously
EasyLoading.showToast(lang.cancelInviteError(e, userId));

Check warning on line 52 in app/lib/features/member/actions/invite_actions.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/member/actions/invite_actions.dart#L52

Added line #L52 was not covered by tests
}
}
}
16 changes: 16 additions & 0 deletions app/lib/features/member/providers/invite_providers.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:acter/common/extensions/ref_debounce.dart';
import 'package:acter/features/home/providers/client_providers.dart';
import 'package:acter/features/tasks/providers/notifiers.dart';
import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

Expand Down Expand Up @@ -62,3 +63,18 @@ final filteredSuggestedUsersProvider =
true;
}).toList();
});

// Provider for getting the list of invited users for a task
final taskInvitationsProvider = AsyncNotifierProvider.family<AsyncTaskInvitationsNotifier, List<String>, Task>(
() => AsyncTaskInvitationsNotifier(),
);

// Provider for checking if a task has any invitations
final taskHasInvitationsProvider = AsyncNotifierProvider.family<AsyncTaskHasInvitationsNotifier, bool, Task>(
() => AsyncTaskHasInvitationsNotifier(),
);

// Provider for checking if a user is invited to a task
final taskUserInvitationProvider = AsyncNotifierProvider.family<AsyncTaskUserInvitationNotifier, bool, (Task, String)>(
() => AsyncTaskUserInvitationNotifier(),
);
94 changes: 52 additions & 42 deletions app/lib/features/member/widgets/user_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import 'package:acter/common/providers/room_providers.dart';
import 'package:acter/common/themes/colors/color_scheme.dart';
import 'package:acter/common/utils/utils.dart';
import 'package:acter/features/member/actions/invite_actions.dart';
import 'package:acter/features/member/providers/invite_providers.dart';
import 'package:acter_avatar/acter_avatar.dart';
import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart';
import 'package:atlas_icons/atlas_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:acter/l10n/generated/l10n.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
Expand Down Expand Up @@ -87,6 +88,7 @@
final bool includeSharedRooms;
final bool includeUserJoinState;
final VoidCallback? onTap;
final Task? task;

const UserBuilder({
super.key,
Expand All @@ -96,6 +98,7 @@
this.onTap,
this.includeSharedRooms = false,
this.includeUserJoinState = true,
this.task,
});

@override
Expand All @@ -108,7 +111,7 @@
title: Text(displayName ?? userId),
subtitle: (displayName == null) ? null : Text(userId),
leading: ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 18)),
trailing: _renderTrailing(context, ref),
trailing: _renderTrailing(context, ref, task),
),
);
if (includeSharedRooms) {
Expand All @@ -117,11 +120,28 @@
return tile;
}

Widget? _renderTrailing(BuildContext context, WidgetRef ref) {
Widget? _renderTrailing(BuildContext context, WidgetRef ref, Task? task) {
if (!includeUserJoinState) return null;
return roomId.map((rId) {
final room = ref.watch(maybeRoomProvider(rId)).valueOrNull;
return room.map((r) => UserStateButton(userId: userId, room: r)) ??
return room.map((r) => UserStateButton(
userId: userId,

Check warning on line 128 in app/lib/features/member/widgets/user_builder.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/member/widgets/user_builder.dart#L128

Added line #L128 was not covered by tests
room: r,
onInvite: (userId) => InviteActions.handleInvite(

Check warning on line 130 in app/lib/features/member/widgets/user_builder.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/member/widgets/user_builder.dart#L130

Added line #L130 was not covered by tests
context: context,
ref: ref,
userId: userId,
room: r,
task: task,
),
onCancelInvite: (userId) => InviteActions.handleCancelInvite(

Check warning on line 137 in app/lib/features/member/widgets/user_builder.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/member/widgets/user_builder.dart#L137

Added line #L137 was not covered by tests
context: context,
ref: ref,
userId: userId,
room: r,
),
task: task,
)) ??
const Skeletonizer(child: Text('user'));
});
}
Expand Down Expand Up @@ -207,53 +227,32 @@
class UserStateButton extends ConsumerWidget {
final String userId;
final Room room;
final Future<void> Function(String userId) onInvite;
final Future<void> Function(String userId) onCancelInvite;
final Task? task;
Comment thread
anisha-e10 marked this conversation as resolved.

const UserStateButton({super.key, required this.room, required this.userId});

Future<void> _handleInvite(BuildContext context) async {
final lang = L10n.of(context);
EasyLoading.show(status: lang.invitingLoading(userId), dismissOnTap: false);
try {
await room.inviteUser(userId);
EasyLoading.dismiss();
} catch (e) {
// ignore: use_build_context_synchronously
EasyLoading.showToast(lang.invitingError(e, userId));
}
}

Future<void> _cancelInvite(BuildContext context, WidgetRef ref) async {
final lang = L10n.of(context);
EasyLoading.show(
status: lang.cancelInviteLoading(userId),
dismissOnTap: false,
);
try {
final member =
ref
.read(memberProvider((userId: userId, roomId: room.roomIdStr())))
.valueOrNull;
if (member != null) {
await member.kick('Cancel Invite');
}
EasyLoading.dismiss();
} catch (e) {
// ignore: use_build_context_synchronously
EasyLoading.showToast(lang.cancelInviteError(e, userId));
}
}
const UserStateButton({
super.key,
required this.room,
required this.userId,
required this.onInvite,
required this.onCancelInvite,
this.task,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final lang = L10n.of(context);
final colorScheme = Theme.of(context).colorScheme;
final disabledColor = Theme.of(context).disabledColor;
final roomId = room.roomIdStr();
final invited =
ref.watch(roomInvitedMembersProvider(roomId)).valueOrNull ?? [];
final joined = ref.watch(membersIdsProvider(roomId)).valueOrNull ?? [];
final isUserInvitedForTask = task != null ? ref.watch(taskUserInvitationProvider((task!, userId))).valueOrNull ?? false : false;
if (isInvited(userId, invited)) {
return InkWell(
onTap: () => _cancelInvite(context, ref),
onTap: () => onCancelInvite.call(userId),
child: Chip(
label: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
Expand All @@ -269,7 +268,18 @@
),
);
}
if (isJoined(userId, joined)) {
if (isUserInvitedForTask) {
return Chip(
label: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Text(lang.invited),
),
backgroundColor: disabledColor,
side: BorderSide.none,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
);
}
if (isJoined(userId, joined) && task == null) {
return Chip(
label: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
Expand All @@ -281,7 +291,7 @@
);
}
return InkWell(
onTap: () => _handleInvite(context),
onTap: () => onInvite.call(userId),
child: Chip(
label: Row(
mainAxisSize: MainAxisSize.min,
Expand All @@ -295,7 +305,7 @@
),
],
),
side: BorderSide(color: Theme.of(context).colorScheme.primary),
side: BorderSide(color: colorScheme.primary),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
Expand Down
49 changes: 49 additions & 0 deletions app/lib/features/tasks/actions/assign_unassign_task.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:acter/features/notifications/actions/autosubscribe.dart';
import 'package:acter/l10n/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart';
import 'package:logging/logging.dart';

final _log = Logger('AssignUnassignTask');

Future<void> onAssign(BuildContext context, WidgetRef ref, Task task) async {
final lang = L10n.of(context);
EasyLoading.show(status: lang.assigningSelf);
try {
await task.assignSelf();
await autosubscribe(ref: ref, objectId: task.eventIdStr(), lang: lang);
EasyLoading.showToast(lang.assignedYourself);

Check warning on line 17 in app/lib/features/tasks/actions/assign_unassign_task.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/tasks/actions/assign_unassign_task.dart#L17

Added line #L17 was not covered by tests
} catch (e, s) {
_log.severe('Failed to self-assign task', e, s);
if (!context.mounted) {
EasyLoading.dismiss();

Check warning on line 21 in app/lib/features/tasks/actions/assign_unassign_task.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/tasks/actions/assign_unassign_task.dart#L21

Added line #L21 was not covered by tests
return;
}
EasyLoading.showError(
lang.failedToAssignSelf(e),
duration: const Duration(seconds: 3),
);
}
}

Future<void> onUnAssign(BuildContext context, WidgetRef ref, Task task) async {
final lang = L10n.of(context);
EasyLoading.show(status: lang.unassigningSelf);
try {
await task.unassignSelf();
await autosubscribe(ref: ref, objectId: task.eventIdStr(), lang: lang);
EasyLoading.showToast(lang.assignmentWithdrawn);

Check warning on line 37 in app/lib/features/tasks/actions/assign_unassign_task.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/tasks/actions/assign_unassign_task.dart#L37

Added line #L37 was not covered by tests
} catch (e, s) {
_log.severe('Failed to self-unassign task', e, s);
if (!context.mounted) {
EasyLoading.dismiss();

Check warning on line 41 in app/lib/features/tasks/actions/assign_unassign_task.dart

View check run for this annotation

Codecov / codecov/patch

app/lib/features/tasks/actions/assign_unassign_task.dart#L41

Added line #L41 was not covered by tests
return;
}
EasyLoading.showError(
lang.failedToUnassignSelf(e),
duration: const Duration(seconds: 3),
);
}
}
Loading
Loading