✨ Auto complete in post as well
This commit is contained in:
@@ -25,4 +25,16 @@ class AutocompleteService {
|
|||||||
final data = response.data as List<dynamic>;
|
final data = response.data as List<dynamic>;
|
||||||
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
|
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<AutocompleteSuggestion>> getGeneralSuggestions(
|
||||||
|
String content,
|
||||||
|
) async {
|
||||||
|
final response = await _client.post(
|
||||||
|
'/sphere/autocomplete',
|
||||||
|
data: {'content': content},
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = response.data as List<dynamic>;
|
||||||
|
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,9 @@ import "package:island/models/account.dart";
|
|||||||
import "package:island/models/autocomplete_response.dart";
|
import "package:island/models/autocomplete_response.dart";
|
||||||
import "package:island/models/chat.dart";
|
import "package:island/models/chat.dart";
|
||||||
import "package:island/models/file.dart";
|
import "package:island/models/file.dart";
|
||||||
|
import "package:island/models/publisher.dart";
|
||||||
|
import "package:island/models/realm.dart";
|
||||||
|
import "package:island/models/sticker.dart";
|
||||||
import "package:island/pods/config.dart";
|
import "package:island/pods/config.dart";
|
||||||
import "package:island/services/autocomplete_service.dart";
|
import "package:island/services/autocomplete_service.dart";
|
||||||
import "package:island/services/responsive.dart";
|
import "package:island/services/responsive.dart";
|
||||||
@@ -448,12 +451,46 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'chatroom':
|
case 'chatroom':
|
||||||
|
final chatRoom = SnChatRoom.fromJson(
|
||||||
|
suggestion.data,
|
||||||
|
);
|
||||||
|
title = chatRoom.name ?? 'Chat Room';
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: chatRoom.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'realm':
|
case 'realm':
|
||||||
|
final realm = SnRealm.fromJson(suggestion.data);
|
||||||
|
title = realm.name;
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: realm.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'publisher':
|
case 'publisher':
|
||||||
|
final publisher = SnPublisher.fromJson(
|
||||||
|
suggestion.data,
|
||||||
|
);
|
||||||
|
title = publisher.name;
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: publisher.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'sticker':
|
case 'sticker':
|
||||||
|
final sticker = SnSticker.fromJson(suggestion.data);
|
||||||
|
title = sticker.slug;
|
||||||
|
leading = ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: CloudImageWidget(
|
||||||
|
fileId: sticker.imageId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,20 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/autocomplete_response.dart';
|
||||||
|
import 'package:island/models/chat.dart';
|
||||||
|
import 'package:island/models/publisher.dart';
|
||||||
|
import 'package:island/models/realm.dart';
|
||||||
|
import 'package:island/models/sticker.dart';
|
||||||
|
import 'package:island/services/autocomplete_service.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.dart';
|
import 'package:island/widgets/post/compose_shared.dart';
|
||||||
|
|
||||||
/// A reusable widget for the form fields in compose screens.
|
/// A reusable widget for the form fields in compose screens.
|
||||||
/// Includes title, description, and content text fields.
|
/// Includes title, description, and content text fields.
|
||||||
class ComposeFormFields extends StatelessWidget {
|
class ComposeFormFields extends HookConsumerWidget {
|
||||||
final ComposeState state;
|
final ComposeState state;
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final bool showPublisherAvatar;
|
final bool showPublisherAvatar;
|
||||||
@@ -20,7 +29,7 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@@ -111,22 +120,125 @@ class ComposeFormFields extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Content field
|
// Content field
|
||||||
TextField(
|
TypeAheadField<AutocompleteSuggestion>(
|
||||||
controller: state.contentController,
|
controller: state.contentController,
|
||||||
enabled: enabled && state.currentPublisher.value != null,
|
builder: (context, controller, focusNode) {
|
||||||
style: theme.textTheme.bodyMedium,
|
return TextField(
|
||||||
decoration: InputDecoration(
|
focusNode: focusNode,
|
||||||
border: InputBorder.none,
|
controller: controller,
|
||||||
hintText: 'postContent'.tr(),
|
enabled: enabled && state.currentPublisher.value != null,
|
||||||
isCollapsed: true,
|
style: theme.textTheme.bodyMedium,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
decoration: InputDecoration(
|
||||||
vertical: 8,
|
border: InputBorder.none,
|
||||||
horizontal: 8,
|
hintText: 'postContent'.tr(),
|
||||||
),
|
isCollapsed: true,
|
||||||
),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
maxLines: null,
|
vertical: 8,
|
||||||
onTapOutside:
|
horizontal: 8,
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
),
|
||||||
|
),
|
||||||
|
maxLines: null,
|
||||||
|
onTapOutside:
|
||||||
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
// Only trigger on @ or :
|
||||||
|
final atIndex = pattern.lastIndexOf('@');
|
||||||
|
final colonIndex = pattern.lastIndexOf(':');
|
||||||
|
final triggerIndex =
|
||||||
|
atIndex > colonIndex ? atIndex : colonIndex;
|
||||||
|
if (triggerIndex == -1) return [];
|
||||||
|
final service = ref.read(autocompleteServiceProvider);
|
||||||
|
try {
|
||||||
|
return await service.getGeneralSuggestions(pattern);
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context, suggestion) {
|
||||||
|
String title = 'unknown'.tr();
|
||||||
|
Widget leading = Icon(Icons.help);
|
||||||
|
switch (suggestion.type) {
|
||||||
|
case 'user':
|
||||||
|
final user = SnAccount.fromJson(suggestion.data);
|
||||||
|
title = user.nick;
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: user.profile.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'chatroom':
|
||||||
|
final chatRoom = SnChatRoom.fromJson(suggestion.data);
|
||||||
|
title = chatRoom.name ?? 'Chat Room';
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: chatRoom.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'realm':
|
||||||
|
final realm = SnRealm.fromJson(suggestion.data);
|
||||||
|
title = realm.name;
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: realm.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'publisher':
|
||||||
|
final publisher = SnPublisher.fromJson(suggestion.data);
|
||||||
|
title = publisher.name;
|
||||||
|
leading = ProfilePictureWidget(
|
||||||
|
file: publisher.picture,
|
||||||
|
radius: 18,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'sticker':
|
||||||
|
final sticker = SnSticker.fromJson(suggestion.data);
|
||||||
|
title = sticker.slug;
|
||||||
|
leading = ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: CloudImageWidget(fileId: sticker.imageId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
leading: leading,
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: Text(suggestion.keyword),
|
||||||
|
dense: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSelected: (suggestion) {
|
||||||
|
final text = state.contentController.text;
|
||||||
|
final atIndex = text.lastIndexOf('@');
|
||||||
|
final colonIndex = text.lastIndexOf(':');
|
||||||
|
final triggerIndex =
|
||||||
|
atIndex > colonIndex ? atIndex : colonIndex;
|
||||||
|
if (triggerIndex == -1) return;
|
||||||
|
final newText = text.replaceRange(
|
||||||
|
triggerIndex,
|
||||||
|
text.length,
|
||||||
|
suggestion.keyword,
|
||||||
|
);
|
||||||
|
state.contentController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: triggerIndex + suggestion.keyword.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
direction: VerticalDirection.down,
|
||||||
|
hideOnEmpty: true,
|
||||||
|
hideOnLoading: true,
|
||||||
|
debounceDuration: const Duration(milliseconds: 500),
|
||||||
|
loadingBuilder: (context) => const Text('Loading...'),
|
||||||
|
errorBuilder: (context, error) => const Text('Error!'),
|
||||||
|
emptyBuilder: (context) => const Text('No items found!'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Reference in New Issue
Block a user