Merge pull request #1092 from MateusHBR/feat/open-library-from-limited-access
feat(ui, localization): Open library from limited access
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
|
- [[#1087]](https://github.com/GetStream/stream-chat-flutter/issues/1087): Handle limited access to camera on iOS.
|
||||||
- `centerTitle` and `elevation` properties to `ChannelHeader`, `ThreadHeader` and `ChannelListHeader`.
|
- `centerTitle` and `elevation` properties to `ChannelHeader`, `ThreadHeader` and `ChannelListHeader`.
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -311,6 +311,9 @@ abstract class Translations {
|
|||||||
/// The label for "Reply to message"
|
/// The label for "Reply to message"
|
||||||
String get replyToMessageLabel;
|
String get replyToMessageLabel;
|
||||||
|
|
||||||
|
/// The label for "View library"
|
||||||
|
String get viewLibrary;
|
||||||
|
|
||||||
/// Label for "Attachment limit exceeded:
|
/// Label for "Attachment limit exceeded:
|
||||||
/// it's not possible to add more than $limit attachments"
|
/// it's not possible to add more than $limit attachments"
|
||||||
String attachmentLimitExceedError(int limit);
|
String attachmentLimitExceedError(int limit);
|
||||||
@@ -682,6 +685,9 @@ class DefaultTranslations implements Translations {
|
|||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Slow mode ON';
|
String get slowModeOnLabel => 'Slow mode ON';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'View library';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String attachmentLimitExceedError(int limit) => """
|
String attachmentLimitExceedError(int limit) => """
|
||||||
Attachment limit exceeded: it's not possible to add more than $limit attachments""";
|
Attachment limit exceeded: it's not possible to add more than $limit attachments""";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
extension on Duration {
|
extension on Duration {
|
||||||
@@ -25,6 +26,7 @@ class MediaListView extends StatefulWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
this.selectedIds = const [],
|
this.selectedIds = const [],
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
|
this.controller,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Stores the media selected
|
/// Stores the media selected
|
||||||
@@ -33,18 +35,28 @@ class MediaListView extends StatefulWidget {
|
|||||||
/// Callback for on media selected
|
/// Callback for on media selected
|
||||||
final void Function(AssetEntity media)? onSelect;
|
final void Function(AssetEntity media)? onSelect;
|
||||||
|
|
||||||
|
/// Controller that handles MediaListView
|
||||||
|
final MediaListViewController? controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MediaListViewState createState() => _MediaListViewState();
|
_MediaListViewState createState() => _MediaListViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MediaListViewState extends State<MediaListView> {
|
class _MediaListViewState extends State<MediaListView> {
|
||||||
final _media = <AssetEntity>[];
|
var _media = <AssetEntity>[];
|
||||||
final ScrollController _scrollController = ScrollController();
|
var _currentPage = 0;
|
||||||
int _currentPage = 0;
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
|
/// Controller necessary to verify limited access to photo gallery in iOS and
|
||||||
|
/// update the media list when listerners are emitted
|
||||||
|
late final controller = widget.controller ?? MediaListViewController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => LazyLoadScrollView(
|
Widget build(BuildContext context) => LazyLoadScrollView(
|
||||||
onEndOfPage: () async => _getMedia(),
|
onEndOfPage: () async {
|
||||||
|
await _getMedia();
|
||||||
|
_updatePage();
|
||||||
|
},
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
itemCount: _media.length,
|
itemCount: _media.length,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
@@ -141,9 +153,29 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
controller.addListener(_updateMediaList);
|
||||||
_getMedia();
|
_getMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
controller.removeListener(_updateMediaList);
|
||||||
|
if (widget.controller == null) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateMediaList() {
|
||||||
|
if (controller.shouldUpdateMedia) {
|
||||||
|
_getMedia();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updatePage() {
|
||||||
|
++_currentPage;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _getMedia() async {
|
Future<void> _getMedia() async {
|
||||||
final assetList = (await PhotoManager.getAssetPathList(
|
final assetList = (await PhotoManager.getAssetPathList(
|
||||||
filterOption: FilterOptionGroup(
|
filterOption: FilterOptionGroup(
|
||||||
@@ -162,13 +194,11 @@ class _MediaListViewState extends State<MediaListView> {
|
|||||||
page: _currentPage,
|
page: _currentPage,
|
||||||
size: 50,
|
size: 50,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (media?.isNotEmpty == true) {
|
if (media?.isNotEmpty == true) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_media.addAll(media!);
|
_media = media!;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
++_currentPage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Controller for MediaListView Widget
|
||||||
|
class MediaListViewController extends ChangeNotifier {
|
||||||
|
var _shouldUpdateMedia = false;
|
||||||
|
|
||||||
|
/// Getter that knows if the media should be updated.
|
||||||
|
bool get shouldUpdateMedia => _shouldUpdateMedia;
|
||||||
|
|
||||||
|
/// Method that update shouldUpdateMedia and notify all listeners
|
||||||
|
/// about this update.
|
||||||
|
void updateMedia({required bool newValue}) {
|
||||||
|
_shouldUpdateMedia = newValue;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
|||||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
|
||||||
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||||
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
|
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
|
||||||
@@ -343,6 +344,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
final List<User> _mentionedUsers = [];
|
final List<User> _mentionedUsers = [];
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
|
final _mediaListViewController = MediaListViewController();
|
||||||
late final _focusNode = widget.focusNode ?? FocusNode();
|
late final _focusNode = widget.focusNode ?? FocusNode();
|
||||||
late final _isInternalFocusNode = widget.focusNode == null;
|
late final _isInternalFocusNode = widget.focusNode == null;
|
||||||
bool _inputEnabled = true;
|
bool _inputEnabled = true;
|
||||||
@@ -1056,6 +1058,26 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Spacer(),
|
||||||
|
FutureBuilder(
|
||||||
|
future: PhotoManager.requestPermissionExtend(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData &&
|
||||||
|
snapshot.data == PermissionState.limited) {
|
||||||
|
return TextButton(
|
||||||
|
child: Text(context.translations.viewLibrary),
|
||||||
|
onPressed: () async {
|
||||||
|
await PhotoManager.presentLimited();
|
||||||
|
_mediaListViewController.updateMedia(
|
||||||
|
newValue: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
DecoratedBox(
|
DecoratedBox(
|
||||||
@@ -1088,6 +1110,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: _PickerWidget(
|
child: _PickerWidget(
|
||||||
|
mediaListViewController: _mediaListViewController,
|
||||||
filePickerIndex: _filePickerIndex,
|
filePickerIndex: _filePickerIndex,
|
||||||
streamChatTheme: _streamChatTheme,
|
streamChatTheme: _streamChatTheme,
|
||||||
containsFile: _attachmentContainsFile,
|
containsFile: _attachmentContainsFile,
|
||||||
@@ -1966,6 +1989,7 @@ class _PickerWidget extends StatefulWidget {
|
|||||||
required this.onAddMoreFilesClick,
|
required this.onAddMoreFilesClick,
|
||||||
required this.onMediaSelected,
|
required this.onMediaSelected,
|
||||||
required this.streamChatTheme,
|
required this.streamChatTheme,
|
||||||
|
required this.mediaListViewController,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final int filePickerIndex;
|
final int filePickerIndex;
|
||||||
@@ -1974,6 +1998,7 @@ class _PickerWidget extends StatefulWidget {
|
|||||||
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
||||||
final void Function(AssetEntity) onMediaSelected;
|
final void Function(AssetEntity) onMediaSelected;
|
||||||
final StreamChatThemeData streamChatTheme;
|
final StreamChatThemeData streamChatTheme;
|
||||||
|
final MediaListViewController mediaListViewController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PickerWidgetState createState() => _PickerWidgetState();
|
_PickerWidgetState createState() => _PickerWidgetState();
|
||||||
@@ -2021,7 +2046,9 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaListView(
|
return MediaListView(
|
||||||
|
controller: widget.mediaListViewController,
|
||||||
selectedIds: widget.selectedMedias,
|
selectedIds: widget.selectedMedias,
|
||||||
onSelect: widget.onMediaSelected,
|
onSelect: widget.onMediaSelected,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('should update media', () {
|
||||||
|
final controller = MediaListViewController();
|
||||||
|
|
||||||
|
expect(controller.shouldUpdateMedia, false);
|
||||||
|
|
||||||
|
controller.updateMedia(newValue: true);
|
||||||
|
expect(controller.shouldUpdateMedia, true);
|
||||||
|
|
||||||
|
controller.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should notify listeners on update media', () {
|
||||||
|
final controller = MediaListViewController();
|
||||||
|
|
||||||
|
var callCount = 0;
|
||||||
|
void updateCallsSpy() => callCount++;
|
||||||
|
|
||||||
|
controller.addListener(updateCallsSpy);
|
||||||
|
|
||||||
|
expect(callCount, 0);
|
||||||
|
controller.updateMedia(newValue: false);
|
||||||
|
expect(controller.shouldUpdateMedia, false);
|
||||||
|
expect(callCount, 1);
|
||||||
|
|
||||||
|
controller.updateMedia(newValue: true);
|
||||||
|
expect(controller.shouldUpdateMedia, true);
|
||||||
|
expect(callCount, 2);
|
||||||
|
|
||||||
|
controller
|
||||||
|
..removeListener(updateCallsSpy)
|
||||||
|
..dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## Upcoming
|
||||||
|
|
||||||
|
* Added translations for viewLibrary.
|
||||||
|
|
||||||
## 2.1.0
|
## 2.1.0
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|||||||
@@ -392,6 +392,9 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Slow mode ON';
|
String get slowModeOnLabel => 'Slow mode ON';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'View library';
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
|||||||
@@ -368,4 +368,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Slow mode ON';
|
String get slowModeOnLabel => 'Slow mode ON';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'View library';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,6 +372,9 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
|
|||||||
No es posible añadir más de $limit archivos adjuntos
|
No es posible añadir más de $limit archivos adjuntos
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'Ver Librería';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Modo lento activado';
|
String get slowModeOnLabel => 'Modo lento activado';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,6 +371,9 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
|
|||||||
Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes
|
Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'Voir la bibliothèque';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Mode lent activé';
|
String get slowModeOnLabel => 'Mode lent activé';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
|
|||||||
अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है
|
अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'पुस्तकालय देखिये';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'स्लो मोड चालू';
|
String get slowModeOnLabel => 'स्लो मोड चालू';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,6 +368,9 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
|||||||
Attenzione: il limite massimo di $limit file è stato superato.
|
Attenzione: il limite massimo di $limit file è stato superato.
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'Vedi la biblioteca';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Slowmode attiva';
|
String get slowModeOnLabel => 'Slowmode attiva';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,6 +350,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'スローモードオン';
|
String get slowModeOnLabel => 'スローモードオン';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'ライブラリを表示';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String attachmentLimitExceedError(int limit) => '''
|
String attachmentLimitExceedError(int limit) => '''
|
||||||
添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません
|
添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません
|
||||||
|
|||||||
@@ -352,6 +352,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations {
|
|||||||
String get slowModeOnLabel => '슬로모드 켜짐';
|
String get slowModeOnLabel => '슬로모드 켜짐';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@override
|
||||||
|
String get viewLibrary => '라이브러리 보기';
|
||||||
|
|
||||||
String attachmentLimitExceedError(int limit) =>
|
String attachmentLimitExceedError(int limit) =>
|
||||||
'첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다';
|
'첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,4 +371,7 @@ Não é possível adicionar mais de $limit arquivos de uma vez
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get slowModeOnLabel => 'Modo lento ativado';
|
String get slowModeOnLabel => 'Modo lento ativado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get viewLibrary => 'Ver biblioteca';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user