diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 00ac6a895..70cc5b45f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -12,7 +12,7 @@
+
+
+
+
+
+
+
+
+
+
intentBacklog = new ArrayList<>();
+ private boolean streamCanceled = false;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
+
+ new EventChannel(getFlutterView(), SHARE_STREAM).setStreamHandler(
+ new EventChannel.StreamHandler() {
+ @Override
+ public void onListen(Object args, final EventChannel.EventSink events) {
+ eventSink = events;
+ streamCanceled = false;
+ for (int i = 0; i < intentBacklog.size(); i++) {
+ sendIntent(intentBacklog.remove(i));
+ }
+ }
+
+ @Override
+ public void onCancel(Object args) {
+ eventSink = null;
+ streamCanceled = true;
+ }
+ }
+ );
+
+ sendIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ sendIntent(intent);
+ }
+
+ private void sendIntent(Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SEND)) {
+ if (eventSink == null) {
+ if (!streamCanceled && !intentBacklog.contains(intent)) {
+ intentBacklog.add(intent);
+ }
+ return;
+ }
+
+ Map args = new HashMap<>();
+ if (intent.getType().startsWith("image/")) {
+ Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ uri = copyImageToTempFile(uri);
+ args.put("path", uri.toString());
+ } else if (intent.getType().startsWith("text/")) {
+ args.put("text", intent.getStringExtra(Intent.EXTRA_TEXT));
+ } else {
+ Log.w(getClass().getSimpleName(), "unknown intent type \"" + intent.getType() + "\" received, ignoring");
+ return;
+ }
+ Log.i(getClass().getSimpleName(), "sending intent to flutter");
+ eventSink.success(args);
+ }
+ }
+
+ private Uri copyImageToTempFile(Uri imageUri) {
+ try {
+ InputStream inputStream;
+ if (imageUri.getScheme().equals("content")) {
+ inputStream = this.getContentResolver().openInputStream(imageUri);
+ } else {
+ inputStream = new FileInputStream(new File(imageUri.getPath()));
+ }
+ Bitmap bmp = BitmapFactory.decodeStream(inputStream);
+ inputStream.close();
+ if (bmp == null) return null;
+
+ ByteArrayOutputStream imageDataStream = new ByteArrayOutputStream();
+ bmp.compress(Bitmap.CompressFormat.JPEG, 100, imageDataStream);
+
+ File imageFile = createTemporaryFile(".jpeg");
+ FileOutputStream fileOutput = new FileOutputStream(imageFile);
+ fileOutput.write(imageDataStream.toByteArray());
+ inputStream.close();
+ fileOutput.close();
+
+ return Uri.fromFile(imageFile);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private File createTemporaryFile(String extension) {
+ try {
+ String name = UUID.randomUUID().toString();
+ return File.createTempFile(name, extension, this.getExternalFilesDir(Environment.DIRECTORY_PICTURES));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
}
diff --git a/assets/images/icons/expand-icon.png b/assets/images/icons/expand-icon.png
new file mode 100644
index 000000000..1f3a337bf
Binary files /dev/null and b/assets/images/icons/expand-icon.png differ
diff --git a/ios/OneSignalNotificationServiceExtension/Info.plist b/ios/OneSignalNotificationServiceExtension/Info.plist
index ba021c755..9eca1008a 100644
--- a/ios/OneSignalNotificationServiceExtension/Info.plist
+++ b/ios/OneSignalNotificationServiceExtension/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
XPC!
CFBundleShortVersionString
- 0.0.21
+ 0.0.34
CFBundleVersion
- 21
+ 34
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 8d603742c..cbc13092e 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -24,6 +24,8 @@ PODS:
- OneSignal (2.9.5)
- path_provider (0.0.1):
- Flutter
+ - share (0.5.2):
+ - Flutter
- sqflite (0.0.1):
- Flutter
- FMDB (~> 2.7.2)
@@ -37,7 +39,7 @@ PODS:
DEPENDENCIES:
- device_info (from `.symlinks/plugins/device_info/ios`)
- - Flutter (from `.symlinks/flutter/ios-release`)
+ - Flutter (from `.symlinks/flutter/ios`)
- flutter_exif_rotation (from `.symlinks/plugins/flutter_exif_rotation/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
@@ -46,6 +48,7 @@ DEPENDENCIES:
- OneSignal (< 3.0, >= 2.9.5)
- onesignal (from `.symlinks/plugins/onesignal/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
+ - share (from `.symlinks/plugins/share/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
@@ -62,7 +65,7 @@ EXTERNAL SOURCES:
device_info:
:path: ".symlinks/plugins/device_info/ios"
Flutter:
- :path: ".symlinks/flutter/ios-release"
+ :path: ".symlinks/flutter/ios"
flutter_exif_rotation:
:path: ".symlinks/plugins/flutter_exif_rotation/ios"
flutter_secure_storage:
@@ -77,6 +80,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/onesignal/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
+ share:
+ :path: ".symlinks/plugins/share/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
uni_links:
@@ -99,6 +104,7 @@ SPEC CHECKSUMS:
OneSignal: ccdeb961882f8668305e5b694e2cb7cb325fc907
onesignal: c2122c20ffcb03d65445f3e0b49273c10f9c37a6
path_provider: 09407919825bfe3c2deae39453b7a5b44f467873
+ share: 222b5dcc8031238af9d7de91149df65bad1aef75
sqflite: d1612813fa7db7c667bed9f1d1b508deffc56999
TOCropViewController: 0a075f02c253e88095143bbac7b013fc6fba5090
uni_links: 5ee5240df5cbffc52d9e7f8017a576b6a6bc5141
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index af421a92a..199bf340e 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -703,7 +703,7 @@
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
- "${PODS_ROOT}/../.symlinks/flutter/ios-release/Flutter.framework",
+ "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index ad08a22fb..27fd7d079 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -17,11 +17,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.0.21
+ 0.0.34
CFBundleSignature
????
CFBundleVersion
- 21
+ 34
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
@@ -35,6 +35,10 @@
We use this permissions to allow you to choose a profile picture or share existing photos in your library.
NSCameraUsageDescription
We use this permissions to allow you to share photos taken within the application.
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ OneSignal, our current push notification library includes code that can use your location, we have disabled this and are waiting on them to update their library to remove this permission completely.
+ NSLocationWhenInUseUsageDescription
+ OneSignal, our current push notification library includes code that can use your location, we have disabled this and are waiting on them to update their library to remove this permission completely.
NSMicrophoneUsageDescription
We use this permission to record audio when you take a video within the application.
NSPhotoLibraryUsageDescription
diff --git a/lib/libs/future_queue.dart b/lib/libs/future_queue.dart
new file mode 100644
index 000000000..3a4ded4c7
--- /dev/null
+++ b/lib/libs/future_queue.dart
@@ -0,0 +1,19 @@
+import 'dart:async';
+
+class FutureQueue {
+ Future _next = new Future.value(null);
+
+ /// Request [operation] to be run exclusively.
+ ///
+ /// Waits for all previously requested operations to complete,
+ /// then runs the operation and completes the returned future with the
+ /// result.
+ /// All creds to https://stackoverflow.com/a/42091982/2608145
+ Future run(Future operation()) {
+ var completer = new Completer();
+ _next.whenComplete(() {
+ completer.complete(new Future.sync(operation));
+ });
+ return _next = completer.future;
+ }
+}
diff --git a/lib/libs/pretty_count.dart b/lib/libs/pretty_count.dart
index 87527b0e7..476263022 100644
--- a/lib/libs/pretty_count.dart
+++ b/lib/libs/pretty_count.dart
@@ -1,6 +1,6 @@
String getPrettyCount(int value) {
String postfix;
- double finalValue = value.toDouble();
+ double finalValue;
if (value < 0) {
throw 'Invalid value';
@@ -17,5 +17,5 @@ String getPrettyCount(int value) {
finalValue = value / 1000000000;
}
- return finalValue.toString() + postfix;
+ return finalValue.round().toString() + postfix;
}
diff --git a/lib/models/badge.dart b/lib/models/badge.dart
index 00d028809..0955c27cf 100644
--- a/lib/models/badge.dart
+++ b/lib/models/badge.dart
@@ -18,6 +18,7 @@ class Badge {
static BadgeKeyword _getBadgeKeywordEnum(keyword) {
switch(keyword) {
+ case 'ANGEL': return BadgeKeyword.angel; break;
case 'VERIFIED': return BadgeKeyword.verified; break;
case 'FOUNDER': return BadgeKeyword.founder; break;
case 'GOLDEN_FOUNDER': return BadgeKeyword.golden_founder; break;
@@ -38,4 +39,4 @@ class Badge {
}
}
-enum BadgeKeyword { verified, founder, golden_founder, diamond_founder, super_founder, none }
\ No newline at end of file
+enum BadgeKeyword { angel, verified, founder, golden_founder, diamond_founder, super_founder, none }
\ No newline at end of file
diff --git a/lib/models/badges_list.dart b/lib/models/badges_list.dart
new file mode 100644
index 000000000..45f9e2222
--- /dev/null
+++ b/lib/models/badges_list.dart
@@ -0,0 +1,18 @@
+import 'package:Openbook/models/badge.dart';
+
+class BadgesList {
+ final List badges;
+
+ BadgesList({
+ this.badges,
+ });
+
+ factory BadgesList.fromJson(List parsedJson) {
+ List badges =
+ parsedJson.map((badgeJson) => Badge.fromJson(badgeJson)).toList();
+
+ return new BadgesList(
+ badges: badges,
+ );
+ }
+}
diff --git a/lib/models/community.dart b/lib/models/community.dart
index df0840be0..010497d8f 100644
--- a/lib/models/community.dart
+++ b/lib/models/community.dart
@@ -37,6 +37,10 @@ class Community extends UpdatableModel {
return result;
}
+ static void clearCache() {
+ factory.clearCache();
+ }
+
final int id;
final User creator;
String name;
@@ -108,6 +112,10 @@ class Community extends UpdatableModel {
return type == CommunityType.private;
}
+ bool isPublic() {
+ return type == CommunityType.public;
+ }
+
bool isAdministrator(User user) {
CommunityMembership membership = getMembershipForUser(user);
if (membership == null) return false;
@@ -244,7 +252,7 @@ class Community extends UpdatableModel {
class CommunityFactory extends UpdatableModelFactory {
@override
SimpleCache cache =
- LruCache(storage: UpdatableModelSimpleStorage(size: 50));
+ SimpleCache(storage: UpdatableModelSimpleStorage(size: 200));
@override
Community makeFromJson(Map json) {
diff --git a/lib/models/notifications/notification.dart b/lib/models/notifications/notification.dart
index 5ec416b6d..6e6a10184 100644
--- a/lib/models/notifications/notification.dart
+++ b/lib/models/notifications/notification.dart
@@ -76,7 +76,7 @@ class OBNotification extends UpdatableModel {
class NotificationFactory extends UpdatableModelFactory {
@override
SimpleCache cache =
- SimpleCache(storage: UpdatableModelSimpleStorage(size: 20));
+ SimpleCache(storage: UpdatableModelSimpleStorage(size: 120));
@override
OBNotification makeFromJson(Map json) {
diff --git a/lib/models/notifications/post_comment_notification.dart b/lib/models/notifications/post_comment_notification.dart
index ad983fb62..6331a64c4 100644
--- a/lib/models/notifications/post_comment_notification.dart
+++ b/lib/models/notifications/post_comment_notification.dart
@@ -12,7 +12,7 @@ class PostCommentNotification {
}
static PostComment _parsePostComment(Map postCommentData) {
- return PostComment.fromJson(postCommentData);
+ return PostComment.fromJSON(postCommentData);
}
int getPostCreatorId() {
diff --git a/lib/models/post.dart b/lib/models/post.dart
index c821d2c6f..fba32b20f 100644
--- a/lib/models/post.dart
+++ b/lib/models/post.dart
@@ -36,6 +36,7 @@ class Post extends UpdatableModel {
bool isMuted;
bool isEncircled;
+ bool isEdited;
static final factory = PostFactory();
@@ -66,7 +67,8 @@ class Post extends UpdatableModel {
this.community,
this.publicReactions,
this.isMuted,
- this.isEncircled})
+ this.isEncircled,
+ this.isEdited})
: super();
void updateFromJson(Map json) {
@@ -94,6 +96,8 @@ class Post extends UpdatableModel {
if (json.containsKey('is_encircled')) isEncircled = json['is_encircled'];
+ if (json.containsKey('is_edited')) isEdited = json['is_edited'];
+
if (json.containsKey('image')) image = factory.parseImage(json['image']);
if (json.containsKey('video')) video = factory.parseVideo(json['video']);
@@ -206,10 +210,6 @@ class Post extends UpdatableModel {
return video.video;
}
- String getText() {
- return text;
- }
-
String getRelativeCreated() {
return timeago.format(created);
}
@@ -281,7 +281,7 @@ class Post extends UpdatableModel {
class PostFactory extends UpdatableModelFactory {
@override
SimpleCache cache =
- LruCache(storage: UpdatableModelSimpleStorage(size: 250));
+ SimpleCache(storage: UpdatableModelSimpleStorage(size: 100));
@override
Post makeFromJson(Map json) {
@@ -304,6 +304,7 @@ class PostFactory extends UpdatableModelFactory {
community: parseCommunity(json['community']),
commentsList: parseCommentList(json['comments']),
isEncircled: json['is_encircled'],
+ isEdited: json['is_edited'],
reactionsEmojiCounts:
parseReactionsEmojiCounts(json['reactions_emoji_counts']));
}
diff --git a/lib/models/post_comment.dart b/lib/models/post_comment.dart
index 95d7c0c8f..76d751efd 100644
--- a/lib/models/post_comment.dart
+++ b/lib/models/post_comment.dart
@@ -1,8 +1,18 @@
import 'package:Openbook/models/post.dart';
import 'package:Openbook/models/user.dart';
+import 'package:dcache/dcache.dart';
import 'package:timeago/timeago.dart' as timeago;
+import 'package:Openbook/models/updatable_model.dart';
+
+class PostComment extends UpdatableModel {
+ final int id;
+ int creatorId;
+ DateTime created;
+ String text;
+ User commenter;
+ Post post;
+ bool isEdited;
-class PostComment {
static convertPostCommentSortTypeToString(PostCommentsSortType type) {
String result;
switch (type) {
@@ -31,45 +41,50 @@ class PostComment {
return type;
}
- final int id;
- final int creatorId;
- final DateTime created;
- final String text;
- final User commenter;
- final Post post;
-
- PostComment({
- this.id,
- this.created,
- this.text,
- this.creatorId,
- this.commenter,
- this.post,
- });
-
- factory PostComment.fromJson(Map parsedJson) {
- DateTime created;
- if (parsedJson.containsKey('created')) {
- created = DateTime.parse(parsedJson['created']).toLocal();
+ static void clearCache() {
+ factory.clearCache();
+ }
+
+ PostComment(
+ {this.id,
+ this.created,
+ this.text,
+ this.creatorId,
+ this.commenter,
+ this.post,
+ this.isEdited});
+
+ static final factory = PostCommentFactory();
+
+ factory PostComment.fromJSON(Map json) {
+ return factory.fromJson(json);
+ }
+
+ @override
+ void updateFromJson(Map json) {
+ if (json.containsKey('commenter')) {
+ commenter = factory.parseUser(json['commenter']);
}
- User commenter;
- if (parsedJson.containsKey('commenter')) {
- commenter = User.fromJson(parsedJson['commenter']);
+ if (json.containsKey('creater_id')) {
+ creatorId = json['creator_id'];
}
- Post post;
- if (parsedJson.containsKey('post')) {
- post = Post.fromJson(parsedJson['post']);
+ if (json.containsKey('is_edited')) {
+ isEdited = json['is_edited'];
}
- return PostComment(
- id: parsedJson['id'],
- creatorId: parsedJson['creator_id'],
- created: created,
- commenter: commenter,
- post: post,
- text: parsedJson['text']);
+ if (json.containsKey('text')) {
+ text = json['text'];
+ }
+
+ if (json.containsKey('post')) {
+ post = factory.parsePost(json['post']);
+ }
+
+ if (json.containsKey('created')) {
+ created = factory.parseCreated(json['created']);
+ }
}
String getRelativeCreated() {
@@ -97,4 +112,37 @@ class PostComment {
}
}
+class PostCommentFactory extends UpdatableModelFactory {
+ @override
+ SimpleCache cache =
+ LruCache(storage: UpdatableModelSimpleStorage(size: 200));
+
+ @override
+ PostComment makeFromJson(Map json) {
+ return PostComment(
+ id: json['id'],
+ creatorId: json['creator_id'],
+ created: parseCreated(json['created']),
+ commenter: parseUser(json['commenter']),
+ post: parsePost(json['post']),
+ isEdited: json['is_edited'],
+ text: json['text']);
+ }
+
+ Post parsePost(Map post) {
+ if (post == null) return null;
+ return Post.fromJson(post);
+ }
+
+ DateTime parseCreated(String created) {
+ if (created == null) return null;
+ return DateTime.parse(created).toLocal();
+ }
+
+ User parseUser(Map userData) {
+ if (userData == null) return null;
+ return User.fromJson(userData);
+ }
+}
+
enum PostCommentsSortType { asc, dec }
diff --git a/lib/models/post_comment_list.dart b/lib/models/post_comment_list.dart
index 3bab3e7b3..0bbe52106 100644
--- a/lib/models/post_comment_list.dart
+++ b/lib/models/post_comment_list.dart
@@ -10,7 +10,7 @@ class PostCommentList {
factory PostCommentList.fromJson(List parsedJson) {
List postComments =
- parsedJson.map((postJson) => PostComment.fromJson(postJson)).toList();
+ parsedJson.map((postJson) => PostComment.fromJSON(postJson)).toList();
return new PostCommentList(
comments: postComments,
diff --git a/lib/models/updatable_model.dart b/lib/models/updatable_model.dart
index 62f2ba47f..4c5b375e7 100644
--- a/lib/models/updatable_model.dart
+++ b/lib/models/updatable_model.dart
@@ -23,7 +23,7 @@ abstract class UpdatableModel {
void updateFromJson(Map json);
- void dispose() {
+ void dipose() {
_updateChangeSubject.close();
}
}
@@ -106,7 +106,8 @@ class UpdatableModelSimpleStorage
@override
void remove(K key) {
CacheEntry item = get(key);
- item.value.dispose();
+ // https://stackoverflow.com/questions/49879438/dart-do-i-have-to-cancel-stream-subscriptions-and-close-streamsinks
+ // item.value.dispose();
this._internalMap.remove(key);
}
@@ -128,3 +129,5 @@ class UpdatableModelSimpleStorage
@override
int get capacity => this._size;
}
+
+typedef void UpdateCallback(Map json);
diff --git a/lib/models/user.dart b/lib/models/user.dart
index 71e0d3de3..0bee2aa95 100644
--- a/lib/models/user.dart
+++ b/lib/models/user.dart
@@ -24,6 +24,7 @@ class User extends UpdatableModel {
int followingCount;
int unreadNotificationsCount;
int postsCount;
+ int inviteCount;
bool isFollowing;
bool isConnected;
bool isFullyConnected;
@@ -77,6 +78,7 @@ class User extends UpdatableModel {
this.followingCount,
this.unreadNotificationsCount,
this.postsCount,
+ this.inviteCount,
this.isFollowing,
this.isConnected,
this.isFullyConnected,
@@ -113,6 +115,7 @@ class User extends UpdatableModel {
if (json.containsKey('unread_notifications_count'))
unreadNotificationsCount = json['unread_notifications_count'];
if (json.containsKey('posts_count')) postsCount = json['posts_count'];
+ if (json.containsKey('invite_count')) inviteCount = json['invite_count'];
if (json.containsKey('is_following')) isFollowing = json['is_following'];
if (json.containsKey('is_connected')) isConnected = json['is_connected'];
if (json.containsKey('connections_circle_id'))
@@ -297,6 +300,7 @@ class UserFactory extends UpdatableModelFactory {
connectionsCircleId: json['connections_circle_id'],
followersCount: json['followers_count'],
postsCount: json['posts_count'],
+ inviteCount: json['invite_count'],
unreadNotificationsCount: json['unread_notifications_count'],
email: json['email'],
username: json['username'],
@@ -309,6 +313,7 @@ class UserFactory extends UpdatableModelFactory {
connectedCircles: parseCircles(json['connected_circles']),
communitiesMemberships:
parseMemberships(json['communities_memberships']),
+ communitiesInvites: parseInvites(json['communities_invites']),
followLists: parseFollowsLists(json['follow_lists']));
}
diff --git a/lib/models/user_invite.dart b/lib/models/user_invite.dart
new file mode 100644
index 000000000..d7ac48349
--- /dev/null
+++ b/lib/models/user_invite.dart
@@ -0,0 +1,102 @@
+import 'package:Openbook/models/updatable_model.dart';
+import 'package:Openbook/models/user.dart';
+import 'package:dcache/dcache.dart';
+
+class UserInvite extends UpdatableModel {
+ final int id;
+ String email;
+ final DateTime created;
+ User createdUser;
+ String nickname;
+ final String token;
+ bool isInviteEmailSent;
+
+ static getShareMessageForInviteWithToken(String token, String apiURL) {
+ const String IOS_DOWNLOAD_LINK = 'https://testflight.apple.com/join/XniAjdyF';
+ const String ANDROID_DOWNLOAD_LINK = 'https://play.google.com/apps/testing/social.openbook.app';
+ String inviteLink = '$apiURL/api/auth/invite?token=$token';
+
+ String message = 'Hey, I\'d like to invite you to Openbook. First, Download the app on iTunes ($IOS_DOWNLOAD_LINK) or the Play store ($ANDROID_DOWNLOAD_LINK). '
+ 'Second, paste this personalised invite link in the \'Sign up\' form in the Openbook App: $inviteLink ';
+
+ return message;
+ }
+
+ static convertUserInviteStatusToBool(UserInviteFilterByStatus value) {
+ bool isPending;
+ switch (value) {
+ case UserInviteFilterByStatus.all:
+ isPending = null;
+ break;
+ case UserInviteFilterByStatus.pending:
+ isPending = true;
+ break;
+ case UserInviteFilterByStatus.accepted:
+ isPending = false;
+ break;
+ default:
+ throw 'Unsupported post comment sort type';
+ }
+ return isPending;
+ }
+
+ static void clearCache() {
+ factory.clearCache();
+ }
+
+ static final factory = UserInviteFactory();
+
+ factory UserInvite.fromJSON(Map json) {
+ return factory.fromJson(json);
+ }
+
+ UserInvite({this.id, this.email, this.created, this.createdUser, this.nickname, this.token, this.isInviteEmailSent});
+
+ @override
+ void updateFromJson(Map json) {
+ if (json.containsKey('email')) {
+ email = json['email'];
+ }
+
+ if (json.containsKey('created_user')) {
+ createdUser = factory.parseUser(json['created_user']);
+ }
+
+ if (json.containsKey('nickname')) {
+ nickname = json['nickname'];
+ }
+
+ if (json.containsKey('is_invite_email_sent')) {
+ isInviteEmailSent = json['is_invite_email_sent'];
+ }
+ }
+}
+
+class UserInviteFactory extends UpdatableModelFactory {
+ @override
+ SimpleCache cache =
+ LruCache(storage: UpdatableModelSimpleStorage(size: 10));
+
+ @override
+ UserInvite makeFromJson(Map json) {
+ DateTime created;
+ var createdData = json['created'];
+ if (createdData != null) created = DateTime.parse(createdData).toLocal();
+
+ return UserInvite(
+ id: json['id'],
+ email: json['email'],
+ created: created,
+ createdUser: parseUser(json['created_user']),
+ nickname: json['nickname'],
+ token: json['token'],
+ isInviteEmailSent: json['is_invite_email_sent']);
+ }
+
+ User parseUser(Map userData) {
+ if (userData == null) return null;
+ return User.fromJson(userData);
+ }
+}
+
+enum UserInviteFilterByStatus { pending, accepted, all }
diff --git a/lib/models/user_invites_list.dart b/lib/models/user_invites_list.dart
new file mode 100644
index 000000000..6a06be200
--- /dev/null
+++ b/lib/models/user_invites_list.dart
@@ -0,0 +1,18 @@
+import 'package:Openbook/models/user_invite.dart';
+
+class UserInvitesList {
+ final List invites;
+
+ UserInvitesList({
+ this.invites,
+ });
+
+ factory UserInvitesList.fromJson(List parsedJson) {
+ List userInvites =
+ parsedJson.map((inviteJson) => UserInvite.fromJSON(inviteJson)).toList();
+
+ return new UserInvitesList(
+ invites: userInvites,
+ );
+ }
+}
diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart
index a00140215..eecf1de86 100644
--- a/lib/models/user_profile.dart
+++ b/lib/models/user_profile.dart
@@ -1,4 +1,5 @@
import 'package:Openbook/models/badge.dart';
+import 'package:Openbook/models/badges_list.dart';
class UserProfile {
final int id;
@@ -23,13 +24,6 @@ class UserProfile {
this.followersCountVisible});
factory UserProfile.fromJSON(Map parsedJson) {
- List badgesList;
- if (parsedJson.containsKey('badges')) {
- List badges = parsedJson['badges'];
- badgesList =
- badges.map((badgeJson) => Badge.fromJson(badgeJson)).toList();
- }
-
return UserProfile(
id: parsedJson['id'],
name: parsedJson['name'],
@@ -38,10 +32,15 @@ class UserProfile {
bio: parsedJson['bio'],
url: parsedJson['url'],
location: parsedJson['location'],
- badges: badgesList,
+ badges: parseBadges(parsedJson['badges']),
followersCountVisible: parsedJson['followers_count_visible']);
}
+ static List parseBadges(List badges) {
+ if (badges == null) return null;
+ return BadgesList.fromJson(badges).badges;
+ }
+
void updateFromJson(Map json) {
if (json.containsKey('name')) name = json['name'];
if (json.containsKey('avatar')) avatar = json['avatar'];
@@ -49,6 +48,7 @@ class UserProfile {
if (json.containsKey('bio')) bio = json['bio'];
if (json.containsKey('url')) url = json['url'];
if (json.containsKey('location')) location = json['location'];
+ if (json.containsKey('badges')) badges = parseBadges(json['badges']);
if (json.containsKey('followers_count_visible'))
followersCountVisible = json['followers_count_visible'];
}
diff --git a/lib/pages/auth/login.dart b/lib/pages/auth/login.dart
index 0721236d1..99fc26555 100644
--- a/lib/pages/auth/login.dart
+++ b/lib/pages/auth/login.dart
@@ -249,6 +249,7 @@ class OBAuthLoginPageState extends State {
contentPadding: inputContentPadding,
labelText: usernameInputLabel,
border: OutlineInputBorder(),
+ errorMaxLines: 3
),
autocorrect: false,
),
diff --git a/lib/pages/auth/reset_password/forgot_password_step.dart b/lib/pages/auth/reset_password/forgot_password_step.dart
index 21eda9999..0f9528085 100644
--- a/lib/pages/auth/reset_password/forgot_password_step.dart
+++ b/lib/pages/auth/reset_password/forgot_password_step.dart
@@ -232,6 +232,7 @@ class OBAuthForgotPasswordPageState extends State {
contentPadding: inputContentPadding,
labelText: usernameInputLabel,
border: OutlineInputBorder(),
+ errorMaxLines: 3
),
autocorrect: false,
),
@@ -252,6 +253,7 @@ class OBAuthForgotPasswordPageState extends State {
contentPadding: inputContentPadding,
labelText: emailInputLabel,
border: OutlineInputBorder(),
+ errorMaxLines: 3
),
autocorrect: false,
),
diff --git a/lib/pages/home/bottom_sheets/comment_more_actions.dart b/lib/pages/home/bottom_sheets/comment_more_actions.dart
new file mode 100644
index 000000000..be80e6afd
--- /dev/null
+++ b/lib/pages/home/bottom_sheets/comment_more_actions.dart
@@ -0,0 +1,99 @@
+import 'package:Openbook/models/post.dart';
+import 'package:Openbook/models/post_comment.dart';
+import 'package:Openbook/models/user.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/modal_service.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/material.dart';
+
+class OBCommentMoreActionsBottomSheet extends StatefulWidget {
+ final PostComment postComment;
+ final Post post;
+
+ const OBCommentMoreActionsBottomSheet({
+ @required this.post,
+ @required this.postComment,
+ Key key})
+ : super(key: key);
+
+ @override
+ State createState() {
+ return OBCommentMoreActionsBottomSheetState();
+ }
+}
+
+class OBCommentMoreActionsBottomSheetState extends State {
+ ToastService _toastService;
+ UserService _userService;
+ ModalService _modalService;
+ bool _requestInProgress;
+
+ @override
+ void initState() {
+ _requestInProgress = false;
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ OpenbookProviderState provider = OpenbookProvider.of(context);
+ _toastService = provider.toastService;
+ _userService = provider.userService;
+ _modalService = provider.modalService;
+ List _moreCommentActions = [];
+
+ User loggedInUser = _userService.getLoggedInUser();
+ bool loggedInUserIsCommunityAdministrator = false;
+ bool loggedInUserIsCommunityModerator = false;
+
+ Post post = widget.post;
+ User postCommenter = widget.postComment.commenter;
+
+ _moreCommentActions.add(
+ ListTile(
+ leading: const OBIcon(OBIcons.editPost),
+ title: const OBText(
+ 'Report comment',
+ ),
+ onTap: _reportPostComment,
+ ),
+ );
+
+
+ return OBPrimaryColorContainer(
+ mainAxisSize: MainAxisSize.min,
+ child: Column(
+ children: _moreCommentActions,
+ mainAxisSize: MainAxisSize.min,
+ ),
+ );
+ }
+
+ void _reportPostComment() async {
+ _toastService.error(message: 'Not implemented yet', context: context);
+ Navigator.pop(context);
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _setRequestInProgress(bool requestInProgress) {
+ setState(() {
+ _requestInProgress = requestInProgress;
+ });
+ }
+}
diff --git a/lib/pages/home/bottom_sheets/photo_picker.dart b/lib/pages/home/bottom_sheets/photo_picker.dart
index 12c826fd7..85fc24ca5 100644
--- a/lib/pages/home/bottom_sheets/photo_picker.dart
+++ b/lib/pages/home/bottom_sheets/photo_picker.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:Openbook/provider.dart';
import 'package:Openbook/services/image_picker.dart';
+import 'package:Openbook/services/toast.dart';
import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/theming/primary_color_container.dart';
import 'package:Openbook/widgets/theming/text.dart';
@@ -17,6 +18,7 @@ class OBPhotoPickerBottomSheet extends StatelessWidget {
Widget build(BuildContext context) {
ImagePickerService imagePickerService =
OpenbookProvider.of(context).imagePickerService;
+ ToastService toastService = OpenbookProvider.of(context).toastService;
List photoPickerActions = [
ListTile(
@@ -25,9 +27,14 @@ class OBPhotoPickerBottomSheet extends StatelessWidget {
'From gallery',
),
onTap: () async {
- File image = await imagePickerService.pickImage(
- imageType: imageType, source: ImageSource.gallery);
- Navigator.pop(context, image);
+ try {
+ File image = await imagePickerService.pickImage(
+ imageType: imageType, source: ImageSource.gallery);
+ Navigator.pop(context, image);
+ } on ImageTooLargeException catch (e) {
+ int limit = e.getLimitInMB();
+ toastService.error(message: 'Image too large (limit: $limit MB)', context: context);
+ }
},
),
ListTile(
@@ -36,19 +43,27 @@ class OBPhotoPickerBottomSheet extends StatelessWidget {
'From camera',
),
onTap: () async {
- File image = await imagePickerService.pickImage(
- imageType: imageType, source: ImageSource.camera);
- Navigator.pop(context, image);
+ try {
+ File image = await imagePickerService.pickImage(
+ imageType: imageType, source: ImageSource.camera);
+ Navigator.pop(context, image);
+ } on ImageTooLargeException catch (e) {
+ int limit = e.getLimitInMB();
+ toastService.error(message: 'Image too large (limit: $limit MB)', context: context);
+ }
},
)
];
return OBPrimaryColorContainer(
mainAxisSize: MainAxisSize.min,
- child: Column(
- children: photoPickerActions,
- mainAxisSize: MainAxisSize.min,
- ),
+ child: Padding(
+ padding: EdgeInsets.only(bottom: 16),
+ child: Column(
+ children: photoPickerActions,
+ mainAxisSize: MainAxisSize.min,
+ ),
+ )
);
}
}
diff --git a/lib/pages/home/bottom_sheets/post_actions.dart b/lib/pages/home/bottom_sheets/post_actions.dart
index f6a1bb45e..6f2825d7a 100644
--- a/lib/pages/home/bottom_sheets/post_actions.dart
+++ b/lib/pages/home/bottom_sheets/post_actions.dart
@@ -1,7 +1,9 @@
+import 'dart:io';
import 'package:Openbook/models/community.dart';
import 'package:Openbook/models/post.dart';
import 'package:Openbook/models/user.dart';
import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/modal_service.dart';
import 'package:Openbook/services/toast.dart';
import 'package:Openbook/services/user.dart';
import 'package:Openbook/services/httpie.dart';
@@ -32,12 +34,14 @@ class OBPostActionsBottomSheet extends StatefulWidget {
class OBPostActionsBottomSheetState extends State {
UserService _userService;
+ ModalService _modalService;
ToastService _toastService;
@override
Widget build(BuildContext context) {
var openbookProvider = OpenbookProvider.of(context);
_userService = openbookProvider.userService;
+ _modalService = openbookProvider.modalService;
_toastService = openbookProvider.toastService;
List postActions = [];
@@ -67,9 +71,20 @@ class OBPostActionsBottomSheetState extends State {
onUnmutedPost: _dismiss,
));
+ if (loggedInUserIsPostCreator) {
+ postActions.add(ListTile(
+ leading: const OBIcon(OBIcons.editPost),
+ title: const OBText(
+ 'Edit post',
+ ),
+ onTap: _onWantsToEditPost,
+ ));
+ }
+
if (loggedInUserIsPostCreator ||
loggedInUserIsCommunityAdministrator ||
loggedInUserIsCommunityModerator) {
+
postActions.add(ListTile(
leading: const OBIcon(OBIcons.deletePost),
title: const OBText(
@@ -107,6 +122,18 @@ class OBPostActionsBottomSheetState extends State {
}
}
+ Future _onWantsToEditPost() async {
+ try {
+ await _modalService.openEditPost(
+ context: context,
+ post: widget.post
+ );
+ Navigator.pop(context);
+ } catch (error) {
+ _onError(error);
+ }
+ }
+
void _onError(error) async {
if (error is HttpieConnectionRefusedError) {
_toastService.error(
diff --git a/lib/pages/home/bottom_sheets/video_picker.dart b/lib/pages/home/bottom_sheets/video_picker.dart
index 409abbb64..c82acd6bd 100644
--- a/lib/pages/home/bottom_sheets/video_picker.dart
+++ b/lib/pages/home/bottom_sheets/video_picker.dart
@@ -40,10 +40,13 @@ class OBVideoPickerBottomSheet extends StatelessWidget {
return OBPrimaryColorContainer(
mainAxisSize: MainAxisSize.min,
- child: Column(
- children: videoPickerActions,
- mainAxisSize: MainAxisSize.min,
- ),
+ child: Padding(
+ padding: EdgeInsets.only(bottom: 16),
+ child: Column(
+ children: videoPickerActions,
+ mainAxisSize: MainAxisSize.min,
+ ),
+ )
);
}
}
diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart
index 974220ce2..5fd4b1140 100644
--- a/lib/pages/home/home.dart
+++ b/lib/pages/home/home.dart
@@ -1,4 +1,6 @@
import 'dart:async';
+import 'dart:io';
+
import 'package:Openbook/models/push_notification.dart';
import 'package:Openbook/pages/home/lib/poppable_page_controller.dart';
import 'package:Openbook/services/intercom.dart';
@@ -13,10 +15,15 @@ import 'package:Openbook/pages/home/pages/search/search.dart';
import 'package:Openbook/pages/home/widgets/bottom-tab-bar.dart';
import 'package:Openbook/pages/home/widgets/own_profile_active_icon.dart';
import 'package:Openbook/pages/home/widgets/tab-scaffold.dart';
+import 'package:Openbook/plugins/share/receive_share_state.dart';
+import 'package:Openbook/plugins/share/share.dart';
import 'package:Openbook/provider.dart';
import 'package:Openbook/services/httpie.dart';
+import 'package:Openbook/services/image_picker.dart';
+import 'package:Openbook/services/modal_service.dart';
import 'package:Openbook/services/toast.dart';
import 'package:Openbook/services/user.dart';
+import 'package:Openbook/services/validation.dart';
import 'package:Openbook/widgets/avatars/avatar.dart';
import 'package:Openbook/widgets/badges/badge.dart';
import 'package:Openbook/widgets/icon.dart';
@@ -31,12 +38,15 @@ class OBHomePage extends StatefulWidget {
}
}
-class OBHomePageState extends State with WidgetsBindingObserver {
+class OBHomePageState extends ReceiveShareState
+ with WidgetsBindingObserver {
static const String oneSignalAppId = '66074bf4-9943-4504-a011-531c2635698b';
UserService _userService;
ToastService _toastService;
PushNotificationsService _pushNotificationsService;
IntercomService _intercomService;
+ ModalService _modalService;
+ ValidationService _validationService;
int _currentIndex;
int _lastIndex;
@@ -60,6 +70,7 @@ class OBHomePageState extends State with WidgetsBindingObserver {
@override
void initState() {
super.initState();
+ enableSharing();
BackButtonInterceptor.add(_backButtonInterceptor);
WidgetsBinding.instance.addObserver(this);
_needsBootstrap = true;
@@ -98,6 +109,8 @@ class OBHomePageState extends State with WidgetsBindingObserver {
_pushNotificationsService = openbookProvider.pushNotificationsService;
_intercomService = openbookProvider.intercomService;
_toastService = openbookProvider.toastService;
+ _modalService = openbookProvider.modalService;
+ _validationService = openbookProvider.validationService;
_bootstrap();
_needsBootstrap = false;
}
@@ -116,6 +129,33 @@ class OBHomePageState extends State with WidgetsBindingObserver {
);
}
+ @override
+ void onShare(Share share) async {
+ String text;
+ File image;
+ if (share.path != null) {
+ image = File.fromUri(Uri.parse(share.path));
+ if (!await _validationService.isImageAllowedSize(
+ image, OBImageType.post)) {
+ int limit = _validationService.getAllowedImageSize(OBImageType.post) ~/
+ 1048576;
+ _toastService.error(
+ message: 'Image too large (limit: $limit MB)', context: context);
+ return;
+ }
+ }
+ if (share.text != null) {
+ text = share.text;
+ if (!_validationService.isPostTextAllowedLength(text)) {
+ _toastService.error(
+ message: 'Text too long (limit: ${ValidationService.POST_MAX_LENGTH} characters)',
+ context: context);
+ return;
+ }
+ }
+ _modalService.openCreatePost(context: context, text: text, image: image);
+ }
+
Widget _getPageForTabIndex(int index) {
Widget page;
switch (OBHomePageTabs.values[index]) {
@@ -349,17 +389,14 @@ class OBHomePageState extends State with WidgetsBindingObserver {
throw 'No tab controller to pop';
}
+ bool canPopRootRoute = Navigator.of(context, rootNavigator: true).canPop();
bool canPopRoute = currentTabController.canPop();
bool preventCloseApp = false;
- if (canPopRoute) {
+ if (canPopRoute && !canPopRootRoute) {
currentTabController.pop();
// Stop default
preventCloseApp = true;
- } else if (currentTab != OBHomePageTabs.timeline) {
-// print('Navigating to timeline');
-// _navigateToTab(OBHomePageTabs.timeline);
-// preventCloseApp = true;
}
// Close the app
diff --git a/lib/pages/home/modals/create_post/create_post.dart b/lib/pages/home/modals/create_post/create_post.dart
index 64dffcdb6..da21b8548 100644
--- a/lib/pages/home/modals/create_post/create_post.dart
+++ b/lib/pages/home/modals/create_post/create_post.dart
@@ -28,8 +28,11 @@ import 'package:pigment/pigment.dart';
class CreatePostModal extends StatefulWidget {
final Community community;
+ final String text;
+ final File image;
- const CreatePostModal({Key key, this.community}) : super(key: key);
+ const CreatePostModal({Key key, this.community, this.text, this.image})
+ : super(key: key);
@override
State createState() {
@@ -45,9 +48,11 @@ class CreatePostModalState extends State {
UserService _userService;
TextEditingController _textController;
+ FocusNode _focusNode;
int _charactersCount;
bool _isPostTextAllowedLength;
+ bool _hasFocus;
bool _hasImage;
bool _hasVideo;
File _postImage;
@@ -64,17 +69,26 @@ class CreatePostModalState extends State {
void initState() {
super.initState();
_textController = TextEditingController();
+ if (widget.text != null) {
+ _textController.text = widget.text;
+ }
_textController.addListener(_onPostTextChanged);
+ _focusNode = FocusNode();
+ _focusNode.addListener(_onFocusNodeChanged);
+ _hasFocus = false;
_charactersCount = 0;
_isPostTextAllowedLength = false;
_hasImage = false;
_hasVideo = false;
- _postItemsWidgets = [OBCreatePostText(controller: _textController)];
+ _postItemsWidgets = [OBCreatePostText(controller: _textController, focusNode: _focusNode)];
if (widget.community != null)
_postItemsWidgets.add(OBPostCommunityPreviewer(
community: widget.community,
));
+ if (widget.image != null) {
+ _setPostImage(widget.image);
+ }
_isCreateCommunityPostInProgress = false;
}
@@ -82,6 +96,7 @@ class CreatePostModalState extends State {
void dispose() {
super.dispose();
_textController.removeListener(_onPostTextChanged);
+ _focusNode.removeListener(_onFocusNodeChanged);
}
@override
@@ -208,8 +223,8 @@ class CreatePostModalState extends State {
if (postActions.isEmpty) return const SizedBox();
return Container(
- height: 51.0,
- padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
+ height: _hasFocus == true ? 51 : 67,
+ padding: EdgeInsets.only(top: 8.0, bottom: _hasFocus == true ? 8 : 24),
color: Color.fromARGB(5, 0, 0, 0),
child: ListView.separated(
physics: const ClampingScrollPhysics(),
@@ -263,6 +278,10 @@ class CreatePostModalState extends State {
});
}
+ void _onFocusNodeChanged() {
+ _hasFocus = _focusNode.hasFocus;
+ }
+
void _setPostImage(File image) {
setState(() {
this._postImage = image;
diff --git a/lib/pages/home/modals/create_post/widgets/create_post_text.dart b/lib/pages/home/modals/create_post/widgets/create_post_text.dart
index ce652b69f..70ef4ca06 100644
--- a/lib/pages/home/modals/create_post/widgets/create_post_text.dart
+++ b/lib/pages/home/modals/create_post/widgets/create_post_text.dart
@@ -5,8 +5,10 @@ import 'package:flutter/material.dart';
class OBCreatePostText extends StatelessWidget {
final TextEditingController controller;
+ final FocusNode focusNode;
+ String hintText;
- OBCreatePostText({this.controller});
+ OBCreatePostText({this.controller, this.focusNode, this.hintText});
@override
Widget build(BuildContext context) {
@@ -23,6 +25,7 @@ class OBCreatePostText extends StatelessWidget {
return TextField(
controller: controller,
autofocus: true,
+ focusNode: focusNode,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.multiline,
maxLines: null,
@@ -31,7 +34,7 @@ class OBCreatePostText extends StatelessWidget {
fontSize: 18.0),
decoration: InputDecoration(
border: InputBorder.none,
- hintText: 'What\'s going on?',
+ hintText: this.hintText != null ? this.hintText : 'What\'s going on?',
hintStyle: TextStyle(
color: themeValueParserService
.parseColor(theme.secondaryTextColor),
diff --git a/lib/pages/home/modals/edit_post/edit_post.dart b/lib/pages/home/modals/edit_post/edit_post.dart
new file mode 100644
index 000000000..7fe63c219
--- /dev/null
+++ b/lib/pages/home/modals/edit_post/edit_post.dart
@@ -0,0 +1,245 @@
+import 'package:Openbook/models/post.dart';
+import 'package:Openbook/pages/home/modals/create_post/widgets/create_post_text.dart';
+import 'package:Openbook/pages/home/modals/create_post/widgets/post_community_previewer.dart';
+import 'package:Openbook/pages/home/modals/create_post/widgets/remaining_post_characters.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/services/validation.dart';
+import 'package:Openbook/widgets/avatars/logged_in_user_avatar.dart';
+import 'package:Openbook/widgets/avatars/avatar.dart';
+import 'package:Openbook/widgets/buttons/button.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_advanced_networkimage/provider.dart';
+
+class EditPostModal extends StatefulWidget {
+ final Post post;
+
+ const EditPostModal({Key key, this.post}) : super(key: key);
+
+ @override
+ State createState() {
+ return EditPostModalState();
+ }
+}
+
+class EditPostModalState extends State {
+ ValidationService _validationService;
+ ToastService _toastService;
+ UserService _userService;
+
+ TextEditingController _textController;
+ FocusNode _focusNode;
+ int _charactersCount;
+
+ bool _isPostTextAllowedLength;
+ String _originalText;
+
+ List _postItemsWidgets;
+
+ bool _isSaveInProgress;
+
+ @override
+ void initState() {
+ super.initState();
+ _originalText = widget.post.hasText() ? widget.post.text : null;
+ _textController = TextEditingController(text: _originalText);
+ _textController.addListener(_onPostTextChanged);
+ _focusNode = FocusNode();
+ _charactersCount = 0;
+ _isPostTextAllowedLength = false;
+ _postItemsWidgets = [
+ OBCreatePostText(controller: _textController, focusNode: _focusNode)
+ ];
+
+ if (widget.post.hasCommunity())
+ _postItemsWidgets.add(OBPostCommunityPreviewer(
+ community: widget.post.community,
+ ));
+
+ if (widget.post.hasImage()) {
+ _setPostImage(widget.post.getImage());
+ }
+ if (widget.post.hasVideo()) {
+ _setPostImage(widget.post.getVideo());
+ }
+ _isSaveInProgress = false;
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _textController.removeListener(_onPostTextChanged);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ _validationService = openbookProvider.validationService;
+ _userService = openbookProvider.userService;
+ _toastService = openbookProvider.toastService;
+
+ return CupertinoPageScaffold(
+ backgroundColor: Colors.transparent,
+ navigationBar: _buildNavigationBar(),
+ child: OBPrimaryColorContainer(
+ child: Column(
+ children: [_buildEditPostContent()],
+ )));
+ }
+
+ Widget _buildNavigationBar() {
+ bool isPrimaryActionButtonIsEnabled = _isPostTextAllowedLength &&
+ _charactersCount > 0 &&
+ (_originalText != _textController.text);
+
+ return OBThemedNavigationBar(
+ leading: GestureDetector(
+ child: const OBIcon(OBIcons.close),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ title: 'Edit post',
+ trailing:
+ _buildPrimaryActionButton(isEnabled: isPrimaryActionButtonIsEnabled),
+ );
+ }
+
+ Widget _buildPrimaryActionButton({bool isEnabled}) {
+ return OBButton(
+ type: OBButtonType.primary,
+ child: Text('Save'),
+ size: OBButtonSize.small,
+ onPressed: _onWantsToSavePost,
+ isDisabled: !isEnabled,
+ isLoading: _isSaveInProgress);
+ }
+
+ void _onWantsToSavePost() async {
+ _setSaveInProgress(true);
+ Post editedPost;
+ try {
+ editedPost = await _userService.editPost(
+ postUuid: widget.post.uuid, text: _textController.text);
+ Navigator.pop(context, editedPost);
+ } catch (error) {
+ _onError(error);
+ } finally {
+ _setSaveInProgress(false);
+ }
+ }
+
+ Widget _buildEditPostContent() {
+ return Expanded(
+ child: Padding(
+ padding: EdgeInsets.only(left: 20.0, top: 20.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Column(
+ children: [
+ OBLoggedInUserAvatar(
+ size: OBAvatarSize.medium,
+ ),
+ const SizedBox(
+ height: 12.0,
+ ),
+ OBRemainingPostCharacters(
+ maxCharacters: ValidationService.POST_MAX_LENGTH,
+ currentCharacters: _charactersCount,
+ ),
+ ],
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ physics: const ClampingScrollPhysics(),
+ child: Padding(
+ padding:
+ EdgeInsets.only(left: 20.0, right: 20.0, bottom: 30.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: _postItemsWidgets)),
+ ),
+ )
+ ],
+ ),
+ ));
+ }
+
+ void _onPostTextChanged() {
+ String text = _textController.text;
+ setState(() {
+ _charactersCount = text.length;
+ _isPostTextAllowedLength =
+ _validationService.isPostTextAllowedLength(text);
+ });
+ }
+
+ void _setPostImage(String imageUrl) {
+ setState(() {
+ var postImageWidget = ClipRRect(
+ borderRadius: BorderRadius.circular(10.0),
+ child: Image(
+ height: 200.0,
+ width: 200.0,
+ fit: BoxFit.cover,
+ image: AdvancedNetworkImage(imageUrl,
+ useDiskCache: true,
+ fallbackAssetImage: 'assets/images/fallbacks/post-fallback.png',
+ retryLimit: 0)),
+ );
+
+ _addPostItemWidget(postImageWidget);
+ });
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ VoidCallback _addPostItemWidget(Widget postItemWidget) {
+ var widgetSpacing = const SizedBox(
+ height: 20.0,
+ );
+
+ List newPostItemsWidgets = List.from(_postItemsWidgets);
+ newPostItemsWidgets.insert(1, widgetSpacing);
+ newPostItemsWidgets.insert(1, postItemWidget);
+
+ _setPostItemsWidgets(newPostItemsWidgets);
+
+ return () {
+ List newPostItemsWidgets = List.from(_postItemsWidgets);
+ newPostItemsWidgets.remove(postItemWidget);
+ newPostItemsWidgets.remove(widgetSpacing);
+ _setPostItemsWidgets(newPostItemsWidgets);
+ };
+ }
+
+ void _setPostItemsWidgets(List postItemsWidgets) {
+ setState(() {
+ _postItemsWidgets = postItemsWidgets;
+ });
+ }
+
+ void _setSaveInProgress(bool saveInProgress) {
+ setState(() {
+ _isSaveInProgress = saveInProgress;
+ });
+ }
+}
diff --git a/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart b/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart
index c28fa8d40..2dda697d7 100644
--- a/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart
+++ b/lib/pages/home/modals/edit_user_profile/edit_user_profile.dart
@@ -308,6 +308,8 @@ class OBEditUserProfileModalState extends State {
}
void _showImageBottomSheet({@required OBImageType imageType}) {
+ ToastService toastService = OpenbookProvider.of(context).toastService;
+
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
@@ -316,20 +318,30 @@ class OBEditUserProfileModalState extends State {
leading: new Icon(Icons.camera_alt),
title: new Text('Camera'),
onTap: () async {
- var image = await _imagePickerService.pickImage(
- source: ImageSource.camera, imageType: imageType);
- _onUserImageSelected(image: image, imageType: imageType);
+ try {
+ var image = await _imagePickerService.pickImage(
+ source: ImageSource.camera, imageType: imageType);
+ _onUserImageSelected(image: image, imageType: imageType);
+ //if (image != null) createAccountBloc.avatar.add(image);
+ } on ImageTooLargeException catch(e) {
+ int limit = e.getLimitInMB();
+ toastService.error(message: 'Image too large (limit: $limit MB)', context: context);
+ }
Navigator.pop(context);
- //if (image != null) createAccountBloc.avatar.add(image);
},
),
new ListTile(
leading: new Icon(Icons.photo_library),
title: new Text('Gallery'),
onTap: () async {
- var image = await _imagePickerService.pickImage(
- source: ImageSource.gallery, imageType: imageType);
- _onUserImageSelected(image: image, imageType: imageType);
+ try {
+ var image = await _imagePickerService.pickImage(
+ source: ImageSource.gallery, imageType: imageType);
+ _onUserImageSelected(image: image, imageType: imageType);
+ } on ImageTooLargeException catch(e) {
+ int limit = e.getLimitInMB();
+ toastService.error(message: 'Image too large (limit: $limit MB)', context: context);
+ }
Navigator.pop(context);
},
)
diff --git a/lib/pages/home/modals/invite_to_community.dart b/lib/pages/home/modals/invite_to_community.dart
index df99cff51..d13b1be4a 100644
--- a/lib/pages/home/modals/invite_to_community.dart
+++ b/lib/pages/home/modals/invite_to_community.dart
@@ -51,6 +51,7 @@ class OBInviteToCommunityModalState extends State {
),
child: OBPrimaryColorContainer(
child: OBHttpList(
+ key: Key('inviteToCommunityUserList'),
listItemBuilder: _buildLinkedUserListItem,
searchResultListItemBuilder: _buildLinkedUserListItem,
listRefresher: _refreshLinkedUsers,
diff --git a/lib/pages/home/modals/post_comment/post-commenter-expanded.dart b/lib/pages/home/modals/post_comment/post-commenter-expanded.dart
new file mode 100644
index 000000000..95dceb9a2
--- /dev/null
+++ b/lib/pages/home/modals/post_comment/post-commenter-expanded.dart
@@ -0,0 +1,200 @@
+import 'package:Openbook/models/community.dart';
+import 'package:Openbook/models/post.dart';
+import 'package:Openbook/models/post_comment.dart';
+import 'package:Openbook/pages/home/modals/create_post/widgets/create_post_text.dart';
+import 'package:Openbook/pages/home/modals/create_post/widgets/remaining_post_characters.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/bottom_sheet.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/services/validation.dart';
+import 'package:Openbook/widgets/avatars/logged_in_user_avatar.dart';
+import 'package:Openbook/widgets/avatars/avatar.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+
+class OBPostCommenterExpandedModal extends StatefulWidget {
+ final Post post;
+ final PostComment postComment;
+
+ const OBPostCommenterExpandedModal({Key key, this.post, this.postComment}) : super(key: key);
+
+ @override
+ State createState() {
+ return OBPostCommenterExpandedModalState();
+ }
+}
+
+class OBPostCommenterExpandedModalState extends State {
+ ValidationService _validationService;
+ ToastService _toastService;
+ UserService _userService;
+
+ TextEditingController _textController;
+ int _charactersCount;
+ bool _isPostCommentTextAllowedLength;
+ bool _isPostCommentTextOriginal;
+ List _postCommentItemsWidgets;
+ String _originalText;
+
+ @override
+ void initState() {
+ super.initState();
+ _textController = TextEditingController(text: widget.postComment != null ? widget.postComment.text: '');
+ _textController.addListener(_onPostCommentTextChanged);
+ _charactersCount = 0;
+ _isPostCommentTextAllowedLength = false;
+ _isPostCommentTextOriginal = false;
+ _originalText = widget.postComment.text;
+ String hintText = widget.post.commentsCount > 0 ? 'Join the conversation..' : 'Start the conversation..';
+ _postCommentItemsWidgets = [OBCreatePostText(controller: _textController, hintText: hintText)];
+
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ _textController.removeListener(_onPostCommentTextChanged);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ _validationService = openbookProvider.validationService;
+ _userService = openbookProvider.userService;
+ _toastService = openbookProvider.toastService;
+
+ return CupertinoPageScaffold(
+ backgroundColor: Colors.transparent,
+ navigationBar: _buildNavigationBar(),
+ child: OBPrimaryColorContainer(
+ child: Column(
+ children: [_buildPostCommentContent()],
+ )));
+ }
+
+ Widget _buildNavigationBar() {
+ bool isPrimaryActionButtonIsEnabled =
+ (_isPostCommentTextAllowedLength && _charactersCount > 0 && !_isPostCommentTextOriginal);
+
+ return OBThemedNavigationBar(
+ leading: GestureDetector(
+ child: const OBIcon(OBIcons.close),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ title: 'Edit comment',
+ trailing:
+ _buildPrimaryActionButton(isEnabled: isPrimaryActionButtonIsEnabled),
+ );
+ }
+
+ Widget _buildPrimaryActionButton({bool isEnabled}) {
+ Widget primaryButton;
+
+ if (isEnabled) {
+ primaryButton = GestureDetector(
+ onTap: _onWantsToSaveComment,
+ child: const OBText('Save'),
+ );
+ } else {
+ primaryButton = Opacity(
+ opacity: 0.5,
+ child: const OBText('Save'),
+ );
+ }
+
+ return primaryButton;
+ }
+
+ void _onWantsToSaveComment() async {
+ PostComment comment;
+ if (widget.postComment != null) {
+ comment = await _userService.editPostComment(
+ post: widget.post,
+ postComment: widget.postComment,
+ text: _textController.text);
+ } else {
+ comment = await _userService.commentPost(
+ post: widget.post,
+ text: _textController.text);
+ }
+
+ if (comment != null) {
+ // Remove modal
+ Navigator.pop(context, comment);
+ }
+ }
+
+ Widget _buildPostCommentContent() {
+ return Expanded(
+ child: Padding(
+ padding: EdgeInsets.only(left: 20.0, top: 20.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Column(
+ children: [
+ OBLoggedInUserAvatar(
+ size: OBAvatarSize.medium,
+ ),
+ const SizedBox(
+ height: 12.0,
+ ),
+ OBRemainingPostCharacters(
+ maxCharacters: ValidationService.POST_COMMENT_MAX_LENGTH,
+ currentCharacters: _charactersCount,
+ ),
+ ],
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ physics: const ClampingScrollPhysics(),
+ child: Padding(
+ padding:
+ EdgeInsets.only(left: 20.0, right: 20.0, bottom: 30.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: _postCommentItemsWidgets)),
+ ),
+ )
+ ],
+ ),
+ ));
+ }
+
+ void _onPostCommentTextChanged() {
+ String text = _textController.text;
+ setState(() {
+ _charactersCount = text.length;
+ _isPostCommentTextAllowedLength =
+ _validationService.isPostCommentAllowedLength(text);
+ _isPostCommentTextOriginal = _originalText == _textController.text;
+ });
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _unfocusTextField() {
+ FocusScope.of(context).requestFocus(new FocusNode());
+ }
+}
diff --git a/lib/pages/home/modals/post_reactions/widgets/post_reaction_list.dart b/lib/pages/home/modals/post_reactions/widgets/post_reaction_list.dart
index 4fa9e2658..4207da4a5 100644
--- a/lib/pages/home/modals/post_reactions/widgets/post_reaction_list.dart
+++ b/lib/pages/home/modals/post_reactions/widgets/post_reaction_list.dart
@@ -9,7 +9,7 @@ import 'package:Openbook/widgets/progress_indicator.dart';
import 'package:Openbook/widgets/tiles/post_reaction_tile.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
-import 'package:loadmore/loadmore.dart';
+import 'package:Openbook/widgets/load_more.dart';
class OBPostReactionList extends StatefulWidget {
// The emoji to show reactions of
diff --git a/lib/pages/home/modals/react_to_post/react_to_post.dart b/lib/pages/home/modals/react_to_post/react_to_post.dart
index c417c8919..95ce25307 100644
--- a/lib/pages/home/modals/react_to_post/react_to_post.dart
+++ b/lib/pages/home/modals/react_to_post/react_to_post.dart
@@ -9,6 +9,7 @@ import 'package:Openbook/services/user.dart';
import 'package:Openbook/widgets/emoji_picker/emoji_picker.dart';
import 'package:Openbook/widgets/theming/primary_color_container.dart';
import 'package:Openbook/widgets/theming/text.dart';
+import 'package:async/async.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -30,6 +31,7 @@ class OBReactToPostBottomSheetState extends State {
ToastService _toastService;
bool _isReactToPostInProgress;
+ CancelableOperation _reactOperation;
@override
void initState() {
@@ -37,6 +39,12 @@ class OBReactToPostBottomSheetState extends State {
_isReactToPostInProgress = false;
}
+ @override
+ void dispose() {
+ super.dispose();
+ if (_reactOperation != null) _reactOperation.cancel();
+ }
+
@override
Widget build(BuildContext context) {
var openbookProvider = OpenbookProvider.of(context);
@@ -52,10 +60,16 @@ class OBReactToPostBottomSheetState extends State {
children: [
SizedBox(
height: screenHeight / 3,
- child: OBEmojiPicker(
- hasSearch: false,
- isReactionsPicker: true,
- onEmojiPicked: _onEmojiPicked,
+ child: IgnorePointer(
+ ignoring: _isReactToPostInProgress,
+ child: Opacity(
+ opacity: _isReactToPostInProgress ? 0.5 : 1,
+ child: OBEmojiPicker(
+ hasSearch: false,
+ isReactionsPicker: true,
+ onEmojiPicked: _reactToPost,
+ ),
+ ),
),
)
],
@@ -63,24 +77,24 @@ class OBReactToPostBottomSheetState extends State {
);
}
- void _onEmojiPicked(Emoji pressedEmoji, EmojiGroup emojiGroup) {
- _reactToPost(pressedEmoji, emojiGroup);
- }
-
Future _reactToPost(Emoji emoji, EmojiGroup emojiGroup) async {
if (_isReactToPostInProgress) return null;
_setReactToPostInProgress(true);
try {
- PostReaction postReaction = await _userService.reactToPost(
- post: widget.post, emoji: emoji, emojiGroup: emojiGroup);
+ _reactOperation = CancelableOperation.fromFuture(_userService.reactToPost(
+ post: widget.post, emoji: emoji, emojiGroup: emojiGroup));
+
+ PostReaction postReaction = await _reactOperation.value;
widget.post.setReaction(postReaction);
// Remove modal
Navigator.pop(context);
+ _setReactToPostInProgress(false);
} catch (error) {
_onError(error);
- } finally {
_setReactToPostInProgress(false);
+ } finally {
+ _reactOperation = null;
}
}
diff --git a/lib/pages/home/modals/save_community.dart b/lib/pages/home/modals/save_community.dart
index 397e0b5c9..96811222b 100644
--- a/lib/pages/home/modals/save_community.dart
+++ b/lib/pages/home/modals/save_community.dart
@@ -615,7 +615,9 @@ class OBSaveCommunityModalState extends State {
usersAdjective: _usersAdjectiveController.text,
categories: _categories,
invitesEnabled: _invitesEnabled,
- color: _color);
+ color: _color,
+ avatar: _avatarFile,
+ cover: _coverFile);
}
Future _isNameTaken(String communityName) async {
diff --git a/lib/pages/home/modals/user_invites/create_user_invite.dart b/lib/pages/home/modals/user_invites/create_user_invite.dart
new file mode 100644
index 000000000..8f916dc58
--- /dev/null
+++ b/lib/pages/home/modals/user_invites/create_user_invite.dart
@@ -0,0 +1,208 @@
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/services/validation.dart';
+import 'package:Openbook/widgets/buttons/button.dart';
+import 'package:Openbook/widgets/fields/text_form_field.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:async/async.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBCreateUserInviteModal extends StatefulWidget {
+ final UserInvite userInvite;
+ final bool autofocusNameTextField;
+
+ OBCreateUserInviteModal(
+ {this.userInvite, this.autofocusNameTextField = false});
+
+ @override
+ OBCreateUserInviteModalState createState() {
+ return OBCreateUserInviteModalState();
+ }
+}
+
+class OBCreateUserInviteModalState
+ extends State {
+
+ UserService _userService;
+ ToastService _toastService;
+ NavigationService _navigationService;
+ ValidationService _validationService;
+
+ bool _requestInProgress;
+ bool _formWasSubmitted;
+ bool _formValid;
+ bool _hasExistingUserInvite;
+
+ GlobalKey _formKey;
+
+ TextEditingController _nicknameController;
+
+ CancelableOperation _createUpdateOperation;
+
+ @override
+ void initState() {
+ super.initState();
+ _formValid = true;
+ _requestInProgress = false;
+ _formWasSubmitted = false;
+ _nicknameController = TextEditingController();
+ _formKey = GlobalKey();
+ _hasExistingUserInvite = widget.userInvite != null;
+ if (_hasExistingUserInvite) {
+ _nicknameController.text = widget.userInvite.nickname;
+ }
+
+ _nicknameController.addListener(_updateFormValid);
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ if (_createUpdateOperation != null)
+ _createUpdateOperation.cancel();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ _userService = openbookProvider.userService;
+ _toastService = openbookProvider.toastService;
+ _validationService = openbookProvider.validationService;
+ _navigationService = openbookProvider.navigationService;
+
+ return OBCupertinoPageScaffold(
+ navigationBar: _buildNavigationBar(),
+ child: OBPrimaryColorContainer(
+ child: SingleChildScrollView(
+ physics: AlwaysScrollableScrollPhysics(),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20.0),
+ child: Column(
+ children: [
+ OBTextFormField(
+ textCapitalization:
+ TextCapitalization.sentences,
+ size: OBTextFormFieldSize.large,
+ autofocus: widget.autofocusNameTextField,
+ controller: _nicknameController,
+ decoration: InputDecoration(
+ labelText: 'Nickname',
+ hintText: 'e.g. Jane Doe'),
+ validator: (String userInviteNickname) {
+ if (!_formWasSubmitted) return null;
+ return _validationService.validateUserProfileName(userInviteNickname);
+ }),
+ ],
+ )),
+ ],
+ )),
+ ),
+ ));
+ }
+
+ Widget _buildNavigationBar() {
+ return OBThemedNavigationBar(
+ leading: GestureDetector(
+ child: const OBIcon(OBIcons.close),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ title: _hasExistingUserInvite ? 'Edit invite' : 'Create invite',
+ trailing: OBButton(
+ isDisabled: !_formValid,
+ isLoading: _requestInProgress,
+ size: OBButtonSize.small,
+ onPressed: _submitForm,
+ child: Text('Next'),
+ ));
+ }
+
+ bool _validateForm() {
+ return _formKey.currentState.validate();
+ }
+
+ bool _updateFormValid() {
+ var formValid = _validateForm();
+ _setFormValid(formValid);
+ return formValid;
+ }
+
+ void _submitForm() async {
+ _formWasSubmitted = true;
+
+ var formIsValid = _updateFormValid();
+ if (!formIsValid) return;
+ _setRequestInProgress(true);
+ try {
+
+ _createUpdateOperation = CancelableOperation.fromFuture(_hasExistingUserInvite
+ ? _userService.updateUserInvite(userInvite: widget.userInvite,
+ nickname: _nicknameController.text != widget.userInvite.nickname ? _nicknameController.text : null)
+ : _userService.createUserInvite(
+ nickname: _nicknameController.text));
+
+ UserInvite userInvite = await _createUpdateOperation.value;
+ if (!_hasExistingUserInvite) {
+ _navigateToShareInvite(userInvite);
+ } else {
+ Navigator.of(context).pop(userInvite);
+ }
+ } catch (error) {
+ _onError(error);
+ } finally {
+ _setRequestInProgress(false);
+ _createUpdateOperation = null;
+ }
+ }
+
+ void _navigateToShareInvite(UserInvite userInvite) async {
+ UserInvite sharedInvite = await _navigationService.navigateToShareInvite(
+ context: context,
+ userInvite: userInvite);
+ Navigator.of(context).pop(userInvite);
+ if (sharedInvite != null) {
+ // Remove modal
+ Navigator.of(context).pop(sharedInvite);
+ }
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _setRequestInProgress(bool requestInProgress) {
+ setState(() {
+ _requestInProgress = requestInProgress;
+ });
+ }
+
+ void _setFormValid(bool formValid) {
+ setState(() {
+ _formValid = formValid;
+ });
+ }
+
+}
diff --git a/lib/pages/home/modals/user_invites/send_invite_email.dart b/lib/pages/home/modals/user_invites/send_invite_email.dart
new file mode 100644
index 000000000..acb3e0f1d
--- /dev/null
+++ b/lib/pages/home/modals/user_invites/send_invite_email.dart
@@ -0,0 +1,190 @@
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/services/validation.dart';
+import 'package:Openbook/widgets/buttons/button.dart';
+import 'package:Openbook/widgets/fields/text_form_field.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:async/async.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBSendUserInviteEmailModal extends StatefulWidget {
+ final UserInvite userInvite;
+ final bool autofocusEmailTextField;
+
+ OBSendUserInviteEmailModal(
+ {this.userInvite, this.autofocusEmailTextField = false});
+
+ @override
+ OBSendUserInviteEmailModalState createState() {
+ return OBSendUserInviteEmailModalState();
+ }
+}
+
+class OBSendUserInviteEmailModalState
+ extends State {
+
+ UserService _userService;
+ ToastService _toastService;
+ ValidationService _validationService;
+
+ CancelableOperation _emailOperation;
+
+ bool _requestInProgress;
+ bool _formWasSubmitted;
+ bool _formValid;
+
+ GlobalKey _formKey;
+
+ TextEditingController _emailController;
+
+ @override
+ void dispose() {
+ super.dispose();
+ if (_emailOperation != null) _emailOperation.cancel();
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _formValid = true;
+ _requestInProgress = false;
+ _formWasSubmitted = false;
+ _emailController = TextEditingController();
+ _formKey = GlobalKey();
+ if (widget.userInvite.email != null) {
+ _emailController.text = widget.userInvite.email;
+ }
+
+ _emailController.addListener(_updateFormValid);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ _userService = openbookProvider.userService;
+ _toastService = openbookProvider.toastService;
+ _validationService = openbookProvider.validationService;
+
+ return OBCupertinoPageScaffold(
+ navigationBar: _buildNavigationBar(),
+ child: OBPrimaryColorContainer(
+ child: SingleChildScrollView(
+ physics: AlwaysScrollableScrollPhysics(),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20.0),
+ child: Column(
+ children: [
+ OBTextFormField(
+ textCapitalization:
+ TextCapitalization.sentences,
+ size: OBTextFormFieldSize.large,
+ autofocus: widget.autofocusEmailTextField,
+ controller: _emailController,
+ decoration: InputDecoration(
+ labelText: 'Email',
+ hintText: 'e.g. janedoe@email.com'),
+ validator: (String email) {
+ if (!_formWasSubmitted) return null;
+ return _validationService.validateUserEmail(email);
+ }),
+ ],
+ )),
+ ],
+ )),
+ ),
+ ));
+ }
+
+ Widget _buildNavigationBar() {
+ return OBThemedNavigationBar(
+ leading: GestureDetector(
+ child: const OBIcon(OBIcons.close),
+ onTap: () {
+ Navigator.pop(context);
+ },
+ ),
+ title: 'Email invite',
+ trailing: OBButton(
+ isDisabled: !_formValid,
+ isLoading: _requestInProgress,
+ size: OBButtonSize.small,
+ onPressed: _submitForm,
+ child: Text('Send'),
+ ));
+ }
+
+ bool _validateForm() {
+ return _formKey.currentState.validate();
+ }
+
+ bool _updateFormValid() {
+ var formValid = _validateForm();
+ _setFormValid(formValid);
+ return formValid;
+ }
+
+ void _submitForm() async {
+ _formWasSubmitted = true;
+
+ var formIsValid = _updateFormValid();
+ if (!formIsValid) return;
+ _setRequestInProgress(true);
+ try {
+ _emailOperation = CancelableOperation.fromFuture(
+ _userService.sendUserInviteEmail(
+ widget.userInvite, _emailController.text)
+ );
+ await _emailOperation.value;
+ _showUserInviteSent();
+ Navigator.of(context).pop(widget.userInvite);
+ } catch (error) {
+ _onError(error);
+ } finally {
+ _setRequestInProgress(false);
+ _emailOperation = null;
+ }
+ }
+
+ void _showUserInviteSent() {
+ _toastService.success(message: 'Invite email sent', context: context);
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _setRequestInProgress(bool requestInProgress) {
+ setState(() {
+ _requestInProgress = requestInProgress;
+ });
+ }
+
+ void _setFormValid(bool formValid) {
+ setState(() {
+ _formValid = formValid;
+ });
+ }
+
+}
diff --git a/lib/pages/home/modals/zoomable_photo.dart b/lib/pages/home/modals/zoomable_photo.dart
index 9de22dfcb..6f557e87b 100644
--- a/lib/pages/home/modals/zoomable_photo.dart
+++ b/lib/pages/home/modals/zoomable_photo.dart
@@ -1,9 +1,11 @@
import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:photo_view/photo_view.dart';
import 'package:pigment/pigment.dart';
+import "dart:math" show pi;
class OBZoomablePhotoModal extends StatefulWidget {
final String imageUrl;
@@ -16,37 +18,247 @@ class OBZoomablePhotoModal extends StatefulWidget {
}
}
-class OBZoomablePhotoModalState extends State {
- bool isDismissible;
+class OBZoomablePhotoModalState extends State
+ with SingleTickerProviderStateMixin {
+ bool _isDismissible;
+ AnimationController _controller;
+ Animation _offset;
+ Animation _rotationAnimation;
+ double _rotationAngle;
+ double _rotationDirection;
+ double _posX;
+ double _posY;
+ double _velocityX;
+ double _velocityY;
+ PointerDownEvent startDragDetails;
+ PointerMoveEvent updateDragDetails;
+ static const VELOCITY_THRESHOLD = 10.0;
+
+ // THRESHOLD_SECOND_POINTER_EVENT:
+ // max delta distance above which we classify that another pointer event has begin somewhere
+ // else on the screen, as dist between two drag pointer events will not be above
+ // this threshold (foreg. when scaling with two fingers, we ignore the other pointer to prevent a rebuild)
+ static const THRESHOLD_SECOND_POINTER_EVENT = 50.0;
+
+ // rate at which X and Y will increase while exiting the screen
+ static const EXIT_RATE_MULTIPLIER = 50.0;
+ static const CLOCKWISE = 1.0;
+ static const ANTICLOCKWISE = -1.0;
@override
void initState() {
super.initState();
- isDismissible = true;
+ _controller =
+ AnimationController(vsync: this, duration: Duration(milliseconds: 300));
+ _rotationAngle = 0.0;
+ _rotationDirection = CLOCKWISE;
+ _posX = 0.0;
+ _velocityX = 0.0;
+ _velocityY = 0.0;
+ _posY = 0.0;
+ _isDismissible = true;
}
@override
Widget build(BuildContext context) {
- return OBCupertinoPageScaffold(
- backgroundColor: Color.fromARGB(0, 0, 0, 0),
- child: Stack(
- children: [
- PhotoView(
- backgroundDecoration: BoxDecoration(color: Colors.transparent),
- key: Key(widget.imageUrl),
- enableRotation: false,
- scaleStateChangedCallback: _photoViewScaleStateChangedCallback,
- imageProvider: AdvancedNetworkImage(widget.imageUrl,
- retryLimit: 0,
- useDiskCache: true,
- fallbackAssetImage:
- 'assets/images/fallbacks/post-fallback.png'),
- maxScale: PhotoViewComputedScale.covered,
- minScale: PhotoViewComputedScale.contained,
- ),
- _buildCloseButton()
- ],
- ));
+ return WillPopScope(
+ child: OBCupertinoPageScaffold(
+ backgroundColor: Colors.black26,
+ child: Stack(
+ overflow: Overflow.visible,
+ children: [
+ Listener(
+ child: Stack(
+ overflow: Overflow.visible,
+ children: [_getPositionedZoomableImage()],
+ ),
+ onPointerDown: (PointerDownEvent details) {
+ if (startDragDetails == null) {
+ setState(() {
+ startDragDetails = details;
+ });
+ }
+ },
+ onPointerMove: (PointerMoveEvent updatedDetails) {
+ double deltaY = 0.0;
+ double deltaX = 0.0;
+ if (updateDragDetails == null && startDragDetails != null) {
+ deltaY = updatedDetails.position.dy -
+ startDragDetails.position.dy;
+ deltaX = updatedDetails.position.dx -
+ startDragDetails.position.dx;
+ } else if (updateDragDetails != null) {
+ deltaY = updatedDetails.position.dy -
+ updateDragDetails.position.dy;
+ deltaX = updatedDetails.position.dx -
+ updateDragDetails.position.dx;
+ }
+ _updateDragValues(deltaX, deltaY, updatedDetails);
+ },
+ onPointerUp: (PointerUpEvent details) {
+ _checkIsDismissible();
+ },
+ ),
+ _buildCloseButton()
+ ],
+ )),
+ onWillPop: _dismissModalNoPop,
+ );
+ }
+
+ Widget _getPositionedZoomableImage() {
+ double screenWidth = MediaQuery.of(context).size.width;
+ double screenHeight = MediaQuery.of(context).size.height;
+
+ return Positioned(
+ top: _posY != 0 ? _posY : 0,
+ left: _posX != 0 ? _posX : 0,
+ width: screenWidth,
+ height: screenHeight,
+ child: Transform.rotate(
+ angle: _rotationAngle,
+ child: PhotoView(
+ backgroundDecoration: BoxDecoration(color: Colors.transparent),
+ key: Key(widget.imageUrl),
+ enableRotation: false,
+ scaleStateChangedCallback: _photoViewScaleStateChangedCallback,
+ imageProvider: AdvancedNetworkImage(widget.imageUrl,
+ retryLimit: 0,
+ useDiskCache: true,
+ fallbackAssetImage: 'assets/images/fallbacks/post-fallback.png'),
+ maxScale: PhotoViewComputedScale.covered,
+ minScale: PhotoViewComputedScale.contained,
+ ),
+ ),
+ );
+ }
+
+ void _updateDragValues(
+ double deltaX, double deltaY, PointerMoveEvent updatedDetails) {
+ if (deltaX.abs() > THRESHOLD_SECOND_POINTER_EVENT ||
+ deltaY.abs() > THRESHOLD_SECOND_POINTER_EVENT ||
+ !_isDismissible) return;
+ setState(() {
+ _posX = _posX + deltaX;
+ _posY = _posY + deltaY;
+ updateDragDetails = updatedDetails;
+ });
+ _updateRotationValues();
+ // Last reading of velocity is low (below threshold) as the finger leaves the screen,
+ // which causes the dismiss to be cancelled, so we update it lazily.
+ _updateVelocityLazily(deltaX, deltaY);
+ }
+
+ void _updateVelocityLazily(double deltaX, double deltaY) async {
+ Future.delayed(Duration(milliseconds: 0), () {
+ setState(() {
+ _velocityX = deltaX;
+ _velocityY = deltaY;
+ });
+ });
+ }
+
+ void _setBackToOrginalPosition() {
+ setState(() {
+ _offset =
+ Tween(begin: Offset(_posX, _posY), end: Offset(0.0, 0.0))
+ .chain(CurveTween(curve: Curves.easeInOutSine))
+ .animate(_controller)
+ ..addListener(() {
+ _posX = _offset.value.dx;
+ _posY = _offset.value.dy;
+ setState(() {});
+ });
+ startDragDetails = null;
+ updateDragDetails = null;
+ });
+ _rotationAnimation = Tween(begin: _rotationAngle, end: 0.0)
+ .chain(CurveTween(curve: Curves.easeInOutCubic))
+ .animate(_controller)
+ ..addListener(() {
+ _rotationAngle = _rotationAnimation.value;
+ setState(() {});
+ });
+ _controller.reset();
+ _controller.forward();
+ }
+
+ void _checkIsDismissible() {
+ if (_velocityX.abs() > VELOCITY_THRESHOLD ||
+ _velocityY.abs() > VELOCITY_THRESHOLD) {
+ _setTweensWithVelocity();
+ _dismissModal();
+ } else {
+ _setBackToOrginalPosition();
+ }
+ }
+
+ void _updateRotationValues() {
+ if (startDragDetails == null) return;
+ double screenWidth = MediaQuery.of(context).size.width;
+ double screenHeight = MediaQuery.of(context).size.height;
+ double screenMid = screenWidth / 2;
+ double maxRotationAngle = pi / 2;
+ // Rotation increases proportional to distance from mid of screen
+ double rotationRatio =
+ (startDragDetails.position.dx - screenMid).abs() / screenMid;
+
+ if (startDragDetails.position.dx < screenMid) {
+ maxRotationAngle = -pi / 2;
+ }
+ // Rotation increases proportional to drag in Y direction
+ double distanceRatio = _posY / screenHeight;
+
+ double rotationDirection;
+ if ((maxRotationAngle < 0 && _velocityY < 0) ||
+ (maxRotationAngle > 0 && _velocityY > 0)) {
+ rotationDirection = CLOCKWISE;
+ } else {
+ rotationDirection = ANTICLOCKWISE;
+ }
+ setState(() {
+ _rotationAngle = distanceRatio * rotationRatio * maxRotationAngle;
+ _rotationDirection = rotationDirection;
+ });
+ }
+
+ void _setTweensWithVelocity() {
+ setState(() {
+ _offset = Tween(
+ begin: Offset(_posX, _posY),
+ end: Offset(_velocityX * EXIT_RATE_MULTIPLIER,
+ _velocityY * EXIT_RATE_MULTIPLIER))
+ .chain(CurveTween(curve: Curves.easeInOutSine))
+ .animate(_controller)
+ ..addListener(() {
+ _posX = _offset.value.dx + _velocityX / 2;
+ _posY = _offset.value.dy + _velocityY / 2;
+ setState(() {});
+ });
+
+ _rotationAnimation = Tween(
+ begin: _rotationAngle, end: 1.5 * pi * _rotationDirection)
+ .chain(CurveTween(curve: Curves.easeInOutCubic))
+ .animate(_controller)
+ ..addListener(() {
+ _rotationAngle = _rotationAnimation.value;
+ setState(() {});
+ });
+
+ startDragDetails = null;
+ updateDragDetails = null;
+ });
+ _controller.reset();
+ }
+
+ Future _dismissModal() async {
+ await _controller.forward();
+ Navigator.pop(context);
+ }
+
+ Future _dismissModalNoPop() async {
+ _dismissModal();
+ return false;
}
Widget _buildCloseButton() {
@@ -54,51 +266,47 @@ class OBZoomablePhotoModalState extends State {
bottom: 50,
left: 0,
right: 0,
- child: AnimatedOpacity(
- opacity: (isDismissible ? 1 : 0),
- duration: Duration(milliseconds: 50),
- child: SafeArea(
- child: Column(
- children: [
- GestureDetector(
- onTapDown: (tap) {
- Navigator.pop(context);
- },
- child: Container(
- padding: EdgeInsets.all(10),
- decoration: BoxDecoration(
- color: Pigment.fromString('#1d1d1d'),
- borderRadius: BorderRadius.circular(50)),
- child: const OBIcon(
- OBIcons.close,
- size: OBIconSize.large,
- color: Colors.white,
- ),
+ child: SafeArea(
+ child: Column(
+ children: [
+ GestureDetector(
+ onTapDown: (tap) {
+ Navigator.pop(context);
+ },
+ child: Container(
+ padding: EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: Colors.black87,
+ borderRadius: BorderRadius.circular(50)),
+ child: const OBIcon(
+ OBIcons.close,
+ size: OBIconSize.large,
+ color: Colors.white,
),
- )
- ],
- )),
- ),
+ ),
+ )
+ ],
+ )),
);
}
void _photoViewScaleStateChangedCallback(PhotoViewScaleState state) {
switch (state) {
case PhotoViewScaleState.initial:
- setIsDismissible(true);
+ _setIsDismissible(true);
break;
default:
- setIsDismissible(false);
+ _setIsDismissible(false);
}
}
- void setIsDismissible(bool isDismissible) {
+ void _setIsDismissible(bool isDismissible) {
setState(() {
- this.isDismissible = isDismissible;
+ this._isDismissible = isDismissible;
});
}
void toggleIsDismissible() {
- setIsDismissible(!isDismissible);
+ _setIsDismissible(!_isDismissible);
}
}
diff --git a/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart b/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart
index 723cde7cc..fa28969cd 100644
--- a/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart
+++ b/lib/pages/home/pages/communities/widgets/my_communities/widgets/my_communities_group.dart
@@ -8,6 +8,7 @@ import 'package:Openbook/widgets/http_list.dart';
import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/theming/secondary_text.dart';
import 'package:Openbook/widgets/theming/text.dart';
+import 'package:async/async.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -47,6 +48,7 @@ class OBMyCommunitiesGroupState extends State {
NavigationService _navigationService;
List _communityGroupList;
bool _refreshInProgress;
+ CancelableOperation _refreshOperation;
@override
void initState() {
@@ -113,6 +115,7 @@ class OBMyCommunitiesGroupState extends State {
void dispose() {
super.dispose();
if (widget.controller != null) widget.controller.detach();
+ if (_refreshOperation != null) _refreshOperation.cancel();
}
Widget _buildSeeAllButton() {
@@ -153,14 +156,19 @@ class OBMyCommunitiesGroupState extends State {
}
Future _refreshJoinedCommunities() async {
+ if (_refreshOperation != null) _refreshOperation.cancel();
_setRefreshInProgress(true);
try {
- List groupCommunities =
- await widget.communityGroupListRefresher();
+ _refreshOperation =
+ CancelableOperation.fromFuture(widget.communityGroupListRefresher());
+
+ List groupCommunities = await _refreshOperation.value;
+
_setCommunityGroupList(groupCommunities);
} catch (error) {
_onError(error);
} finally {
+ _refreshOperation = null;
_setRefreshInProgress(false);
}
}
diff --git a/lib/pages/home/pages/community/community.dart b/lib/pages/home/pages/community/community.dart
index 539ce80a4..9e202d7cf 100644
--- a/lib/pages/home/pages/community/community.dart
+++ b/lib/pages/home/pages/community/community.dart
@@ -20,6 +20,7 @@ import 'package:Openbook/widgets/post/post.dart';
import 'package:Openbook/widgets/progress_indicator.dart';
import 'package:Openbook/widgets/theming/primary_color_container.dart';
import 'package:Openbook/widgets/theming/text.dart';
+import 'package:async/async.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_pagewise/flutter_pagewise.dart';
@@ -61,6 +62,9 @@ class OBCommunityPageState extends State
// https://github.com/AbdulRahmanAlHamali/flutter_pagewise/issues/55
Post _createdPostToInsertOnNextRefresh;
+ CancelableOperation _loadMorePostsOperation;
+ CancelableOperation _refreshCommunityOperation;
+
@override
void initState() {
super.initState();
@@ -76,6 +80,13 @@ class OBCommunityPageState extends State
_pageStorageKey = PageStorageKey(TabBar);
}
+ @override
+ void dispose() {
+ super.dispose();
+ if (_loadMorePostsOperation != null) _loadMorePostsOperation.cancel();
+ if (_refreshCommunityOperation != null) _refreshCommunityOperation.cancel();
+ }
+
@override
Widget build(BuildContext context) {
if (_needsBootstrap) {
@@ -350,7 +361,10 @@ class OBCommunityPageState extends State
}
Future _refreshCommunity() async {
- var community = await _userService.getCommunityWithName(_community.name);
+ if (_refreshCommunityOperation != null) _refreshCommunityOperation.cancel();
+ _refreshCommunityOperation = CancelableOperation.fromFuture(
+ _userService.getCommunityWithName(_community.name));
+ var community = await _refreshCommunityOperation.value;
_setCommunity(community);
}
@@ -370,6 +384,8 @@ class OBCommunityPageState extends State
return currentPosts;
}
+ if (_loadMorePostsOperation != null) _loadMorePostsOperation.cancel();
+
List morePosts = [];
int lastPostId;
@@ -383,12 +399,14 @@ class OBCommunityPageState extends State
}
try {
- morePosts = (await _userService.getPostsForCommunity(_community,
- maxId: lastPostId))
- .posts;
+ _loadMorePostsOperation = CancelableOperation.fromFuture(
+ _userService.getPostsForCommunity(_community, maxId: lastPostId));
+ morePosts = (await _loadMorePostsOperation.value).posts;
_addPosts(morePosts);
} catch (error) {
_onError(error);
+ } finally {
+ _loadMorePostsOperation = null;
}
return morePosts;
diff --git a/lib/pages/home/pages/community/pages/community_members.dart b/lib/pages/home/pages/community/pages/community_members.dart
new file mode 100644
index 000000000..d59581f2b
--- /dev/null
+++ b/lib/pages/home/pages/community/pages/community_members.dart
@@ -0,0 +1,120 @@
+import 'dart:async';
+
+import 'package:Openbook/models/community.dart';
+import 'package:Openbook/models/user.dart';
+import 'package:Openbook/models/users_list.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/widgets/buttons/actions/follow_button.dart';
+import 'package:Openbook/widgets/http_list.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/tiles/user_tile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBCommunityMembersPage extends StatefulWidget {
+ final Community community;
+
+ const OBCommunityMembersPage({Key key, @required this.community})
+ : super(key: key);
+
+ @override
+ State createState() {
+ return OBCommunityMembersPageState();
+ }
+}
+
+class OBCommunityMembersPageState extends State {
+ UserService _userService;
+ NavigationService _navigationService;
+
+ OBHttpListController _httpListController;
+ bool _needsBootstrap;
+
+ @override
+ void initState() {
+ super.initState();
+ _httpListController = OBHttpListController();
+ _needsBootstrap = true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_needsBootstrap) {
+ var provider = OpenbookProvider.of(context);
+ _userService = provider.userService;
+ _navigationService = provider.navigationService;
+ _needsBootstrap = false;
+ }
+
+ String title = widget.community.usersAdjective ?? 'Community Members';
+ String singularName = widget.community.userAdjective ?? 'member';
+ String pluralName = widget.community.usersAdjective ?? 'members';
+
+ return OBCupertinoPageScaffold(
+ navigationBar: OBThemedNavigationBar(
+ title: title,
+ ),
+ child: OBPrimaryColorContainer(
+ child: OBHttpList(
+ controller: _httpListController,
+ listItemBuilder: _buildCommunityMemberListItem,
+ searchResultListItemBuilder: _buildCommunityMemberListItem,
+ listRefresher: _refreshCommunityMembers,
+ listOnScrollLoader: _loadMoreCommunityMembers,
+ listSearcher: _searchCommunityMembers,
+ resourceSingularName: singularName.toLowerCase(),
+ resourcePluralName: pluralName.toLowerCase(),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCommunityMemberListItem(BuildContext context, User user) {
+ bool isLoggedInUser = _userService.isLoggedInUser(user);
+
+ return OBUserTile(user,
+ onUserTilePressed: _onCommunityMemberListItemPressed,
+ trailing: isLoggedInUser
+ ? null
+ : OBFollowButton(
+ user,
+ size: OBButtonSize.small,
+ unfollowButtonType: OBButtonType.highlight,
+ ));
+ }
+
+ void _onCommunityMemberListItemPressed(User communityMember) {
+ _navigationService.navigateToUserProfile(
+ user: communityMember, context: context);
+ }
+
+ Future> _refreshCommunityMembers() async {
+ UsersList communityMembers =
+ await _userService.getMembersForCommunity(widget.community);
+ return communityMembers.users;
+ }
+
+ Future> _loadMoreCommunityMembers(
+ List communityMembersList) async {
+ var lastCommunityMember = communityMembersList.last;
+ var lastCommunityMemberId = lastCommunityMember.id;
+ var moreCommunityMembers = (await _userService.getMembersForCommunity(
+ widget.community,
+ maxId: lastCommunityMemberId,
+ count: 20,
+ ))
+ .users;
+ return moreCommunityMembers;
+ }
+
+ Future> _searchCommunityMembers(String query) async {
+ UsersList results = await _userService.searchCommunityMembers(
+ query: query, community: widget.community);
+
+ return results.users;
+ }
+}
diff --git a/lib/pages/home/pages/community/widgets/community_card/widgets/community_details/widgets/community_members_count.dart b/lib/pages/home/pages/community/widgets/community_card/widgets/community_details/widgets/community_members_count.dart
index 8387d28b4..ab673d0bb 100644
--- a/lib/pages/home/pages/community/widgets/community_card/widgets/community_details/widgets/community_members_count.dart
+++ b/lib/pages/home/pages/community/widgets/community_card/widgets/community_details/widgets/community_members_count.dart
@@ -23,6 +23,8 @@ class OBCommunityMembersCount extends StatelessWidget {
var openbookProvider = OpenbookProvider.of(context);
var themeService = openbookProvider.themeService;
var themeValueParserService = openbookProvider.themeValueParserService;
+ var userService = openbookProvider.userService;
+ var navigationService = openbookProvider.navigationService;
return StreamBuilder(
stream: themeService.themeChange,
@@ -30,29 +32,42 @@ class OBCommunityMembersCount extends StatelessWidget {
builder: (BuildContext context, AsyncSnapshot snapshot) {
var theme = snapshot.data;
- return Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Flexible(
- child: RichText(
- text: TextSpan(children: [
- TextSpan(
- text: count,
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
- color: themeValueParserService
- .parseColor(theme.primaryTextColor))),
- TextSpan(text: ' '),
- TextSpan(
- text: membersCount == 1 ? userAdjective : usersAdjective,
- style: TextStyle(
- fontSize: 16,
- color: themeValueParserService
- .parseColor(theme.secondaryTextColor)))
- ])),
- ),
- ],
+ return GestureDetector(
+ onTap: () {
+ bool isPublicCommunity = community.isPublic();
+ bool isLoggedInUserMember =
+ community.isMember(userService.getLoggedInUser());
+
+ if (isPublicCommunity || isLoggedInUserMember) {
+ navigationService.navigateToCommunityMembers(
+ community: community, context: context);
+ }
+ },
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Flexible(
+ child: RichText(
+ text: TextSpan(children: [
+ TextSpan(
+ text: count,
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: themeValueParserService
+ .parseColor(theme.primaryTextColor))),
+ TextSpan(text: ' '),
+ TextSpan(
+ text:
+ membersCount == 1 ? userAdjective : usersAdjective,
+ style: TextStyle(
+ fontSize: 16,
+ color: themeValueParserService
+ .parseColor(theme.secondaryTextColor)))
+ ])),
+ ),
+ ],
+ ),
);
});
}
diff --git a/lib/pages/home/pages/menu/menu.dart b/lib/pages/home/pages/menu/menu.dart
index 3d58e6a3a..03cf74d7f 100644
--- a/lib/pages/home/pages/menu/menu.dart
+++ b/lib/pages/home/pages/menu/menu.dart
@@ -1,6 +1,5 @@
import 'package:Openbook/models/user.dart';
import 'package:Openbook/pages/home/lib/poppable_page_controller.dart';
-import 'package:Openbook/pages/home/pages/menu/widgets/curated_themes.dart';
import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
import 'package:Openbook/provider.dart';
@@ -51,13 +50,41 @@ class OBMainMenuPage extends StatelessWidget {
navigationService.navigateToFollowsLists(context: context);
},
),
+ ListTile(
+ leading: const OBIcon(OBIcons.connections),
+ title: const OBText('My invites'),
+ onTap: () {
+ navigationService.navigateToUserInvites(context: context);
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.followers),
+ title: const OBText('My followers'),
+ onTap: () {
+ navigationService.navigateToFollowersPage(context: context);
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.following),
+ title: const OBText('My following'),
+ onTap: () {
+ navigationService.navigateToFollowingPage(context: context);
+ },
+ ),
ListTile(
leading: const OBIcon(OBIcons.settings),
- title: OBText('Account'),
+ title: OBText('Settings'),
onTap: () {
navigationService.navigateToSettingsPage(context: context);
},
),
+ ListTile(
+ leading: const OBIcon(OBIcons.themes),
+ title: OBText('Themes'),
+ onTap: () {
+ navigationService.navigateToThemesPage(context: context);
+ },
+ ),
StreamBuilder(
stream: userService.loggedInUserChange,
initialData: userService.getLoggedInUser(),
@@ -76,6 +103,14 @@ class OBMainMenuPage extends StatelessWidget {
);
},
),
+ ListTile(
+ leading: const OBIcon(OBIcons.link),
+ title: OBText('Useful links'),
+ onTap: () {
+ navigationService.navigateToUsefulLinksPage(
+ context: context);
+ },
+ ),
ListTile(
leading: const OBIcon(OBIcons.logout),
title: OBText(localizationService.trans('DRAWER.LOGOUT')),
@@ -84,8 +119,7 @@ class OBMainMenuPage extends StatelessWidget {
},
)
],
- )),
- OBCuratedThemes()
+ ))
],
),
),
diff --git a/lib/pages/home/pages/menu/pages/connections_circle/connections_circle.dart b/lib/pages/home/pages/menu/pages/connections_circle/connections_circle.dart
index c3dcbf30d..2e93fbe0e 100644
--- a/lib/pages/home/pages/menu/pages/connections_circle/connections_circle.dart
+++ b/lib/pages/home/pages/menu/pages/connections_circle/connections_circle.dart
@@ -38,7 +38,7 @@ class OBConnectionsCirclePageState extends State {
@override
void initState() {
super.initState();
- _isConnectionsCircle = false;
+ _isConnectionsCircle = true;
_refreshIndicatorKey = GlobalKey();
_needsBootstrap = true;
}
diff --git a/lib/pages/home/pages/menu/pages/followers.dart b/lib/pages/home/pages/menu/pages/followers.dart
new file mode 100644
index 000000000..c5120cfb6
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/followers.dart
@@ -0,0 +1,101 @@
+import 'dart:async';
+
+import 'package:Openbook/models/user.dart';
+import 'package:Openbook/models/users_list.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/widgets/buttons/actions/follow_button.dart';
+import 'package:Openbook/widgets/http_list.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/tiles/user_tile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBFollowersPage extends StatefulWidget {
+ @override
+ State createState() {
+ return OBFollowersPageState();
+ }
+}
+
+class OBFollowersPageState extends State {
+ UserService _userService;
+ NavigationService _navigationService;
+
+ OBHttpListController _httpListController;
+ bool _needsBootstrap;
+
+ @override
+ void initState() {
+ super.initState();
+ _httpListController = OBHttpListController();
+ _needsBootstrap = true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_needsBootstrap) {
+ var provider = OpenbookProvider.of(context);
+ _userService = provider.userService;
+ _navigationService = provider.navigationService;
+ _needsBootstrap = false;
+ }
+
+ return OBCupertinoPageScaffold(
+ navigationBar: OBThemedNavigationBar(
+ title: 'Followers',
+ ),
+ child: OBPrimaryColorContainer(
+ child: OBHttpList(
+ controller: _httpListController,
+ listItemBuilder: _buildFollowerListItem,
+ searchResultListItemBuilder: _buildFollowerListItem,
+ listRefresher: _refreshFollowers,
+ listOnScrollLoader: _loadMoreFollowers,
+ listSearcher: _searchFollowers,
+ resourceSingularName: 'follower',
+ resourcePluralName: 'followers',
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFollowerListItem(BuildContext context, User user) {
+ return OBUserTile(user,
+ onUserTilePressed: _onFollowerListItemPressed,
+ trailing: OBFollowButton(
+ user,
+ size: OBButtonSize.small,
+ unfollowButtonType: OBButtonType.highlight,
+ ));
+ }
+
+ void _onFollowerListItemPressed(User follower) {
+ _navigationService.navigateToUserProfile(user: follower, context: context);
+ }
+
+ Future> _refreshFollowers() async {
+ UsersList followers = await _userService.getFollowers();
+ return followers.users;
+ }
+
+ Future> _loadMoreFollowers(List followersList) async {
+ var lastFollower = followersList.last;
+ var lastFollowerId = lastFollower.id;
+ var moreFollowers = (await _userService.getFollowers(
+ maxId: lastFollowerId,
+ count: 20,
+ ))
+ .users;
+ return moreFollowers;
+ }
+
+ Future> _searchFollowers(String query) async {
+ UsersList results = await _userService.searchFollowers(query: query);
+
+ return results.users;
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/following.dart b/lib/pages/home/pages/menu/pages/following.dart
new file mode 100644
index 000000000..3d198004b
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/following.dart
@@ -0,0 +1,101 @@
+import 'dart:async';
+
+import 'package:Openbook/models/user.dart';
+import 'package:Openbook/models/users_list.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/widgets/buttons/actions/follow_button.dart';
+import 'package:Openbook/widgets/http_list.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/tiles/user_tile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBFollowingPage extends StatefulWidget {
+ @override
+ State createState() {
+ return OBFollowingPageState();
+ }
+}
+
+class OBFollowingPageState extends State {
+ UserService _userService;
+ NavigationService _navigationService;
+
+ OBHttpListController _httpListController;
+ bool _needsBootstrap;
+
+ @override
+ void initState() {
+ super.initState();
+ _httpListController = OBHttpListController();
+ _needsBootstrap = true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_needsBootstrap) {
+ var provider = OpenbookProvider.of(context);
+ _userService = provider.userService;
+ _navigationService = provider.navigationService;
+ _needsBootstrap = false;
+ }
+
+ return OBCupertinoPageScaffold(
+ navigationBar: OBThemedNavigationBar(
+ title: 'Following',
+ ),
+ child: OBPrimaryColorContainer(
+ child: OBHttpList(
+ controller: _httpListController,
+ listItemBuilder: _buildFollowingListItem,
+ searchResultListItemBuilder: _buildFollowingListItem,
+ listRefresher: _refreshFollowing,
+ listOnScrollLoader: _loadMoreFollowing,
+ listSearcher: _searchFollowing,
+ resourceSingularName: 'following',
+ resourcePluralName: 'following',
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFollowingListItem(BuildContext context, User user) {
+ return OBUserTile(user,
+ onUserTilePressed: _onFollowingListItemPressed,
+ trailing: OBFollowButton(
+ user,
+ size: OBButtonSize.small,
+ unfollowButtonType: OBButtonType.highlight,
+ ));
+ }
+
+ void _onFollowingListItemPressed(User following) {
+ _navigationService.navigateToUserProfile(user: following, context: context);
+ }
+
+ Future> _refreshFollowing() async {
+ UsersList following = await _userService.getFollowings();
+ return following.users;
+ }
+
+ Future> _loadMoreFollowing(List followingList) async {
+ var lastFollowing = followingList.last;
+ var lastFollowingId = lastFollowing.id;
+ var moreFollowing = (await _userService.getFollowings(
+ maxId: lastFollowingId,
+ count: 20,
+ ))
+ .users;
+ return moreFollowing;
+ }
+
+ Future> _searchFollowing(String query) async {
+ UsersList results = await _userService.searchFollowings(query: query);
+
+ return results.users;
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/settings/pages/account_settings/account_settings.dart b/lib/pages/home/pages/menu/pages/settings/pages/account_settings/account_settings.dart
new file mode 100644
index 000000000..cc7d7d100
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/settings/pages/account_settings/account_settings.dart
@@ -0,0 +1,70 @@
+import 'package:Openbook/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change-password/change_password.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change_email/change_email.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBAccountSettingsPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ var localizationService = openbookProvider.localizationService;
+ var navigationService = openbookProvider.navigationService;
+
+ return CupertinoPageScaffold(
+ backgroundColor: Color.fromARGB(0, 0, 0, 0),
+ navigationBar: OBThemedNavigationBar(title: 'Settings'),
+ child: OBPrimaryColorContainer(
+ child: ListView(
+ physics: const ClampingScrollPhysics(),
+ // Important: Remove any padding from the ListView.
+ padding: EdgeInsets.zero,
+ children: [
+ ListTile(
+ leading: const OBIcon(OBIcons.email),
+ title: OBText(localizationService.trans('SETTINGS.CHANGE_EMAIL')),
+ onTap: () {
+ Navigator.of(context).push(MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (BuildContext context) => Material(
+ child: OBChangeEmailModal(),
+ )));
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.lock),
+ title:
+ OBText(localizationService.trans('SETTINGS.CHANGE_PASSWORD')),
+ onTap: () {
+ Navigator.of(context).push(MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (BuildContext context) => Material(
+ child: OBChangePasswordModal(),
+ )));
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.notifications),
+ title: OBText('Notifications'),
+ onTap: () {
+ navigationService.navigateToNotificationsSettings(
+ context: context);
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.deleteCommunity),
+ title: OBText('Delete account'),
+ onTap: () {
+ navigationService.navigateToDeleteAccount(context: context);
+ },
+ )
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/settings/modals/change-password/change_password.dart b/lib/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change-password/change_password.dart
similarity index 100%
rename from lib/pages/home/pages/menu/pages/settings/modals/change-password/change_password.dart
rename to lib/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change-password/change_password.dart
diff --git a/lib/pages/home/pages/menu/pages/settings/modals/change_email/change_email.dart b/lib/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change_email/change_email.dart
similarity index 93%
rename from lib/pages/home/pages/menu/pages/settings/modals/change_email/change_email.dart
rename to lib/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change_email/change_email.dart
index 492a19575..3abd81325 100644
--- a/lib/pages/home/pages/menu/pages/settings/modals/change_email/change_email.dart
+++ b/lib/pages/home/pages/menu/pages/settings/pages/account_settings/modals/change_email/change_email.dart
@@ -10,6 +10,7 @@ import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
import 'package:Openbook/widgets/page_scaffold.dart';
import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:async/async.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -33,6 +34,7 @@ class OBChangeEmailModalState extends State {
bool _changedEmailTaken = false;
bool _formValid = true;
TextEditingController _emailController = TextEditingController();
+ CancelableOperation _requestOperation;
@override
void initState() {
@@ -44,6 +46,12 @@ class OBChangeEmailModalState extends State {
_emailController.addListener(_updateFormValid);
}
+ @override
+ void dispose() {
+ super.dispose();
+ if (_requestOperation != null) _requestOperation.cancel();
+ }
+
@override
Widget build(BuildContext context) {
var openbookProvider = OpenbookProvider.of(context);
@@ -122,13 +130,9 @@ class OBChangeEmailModalState extends State {
_setRequestInProgress(true);
try {
var email = _emailController.text;
- var originalEmail = _userService.getLoggedInUser().getEmail();
- User user = await _userService.updateUserEmail(email);
- if (user.getEmail() != email || originalEmail == user.getEmail()) {
- _setChangedEmailTaken(true);
- _validateForm();
- return;
- }
+ _requestOperation =
+ CancelableOperation.fromFuture(_userService.updateUserEmail(email));
+ await _requestOperation.value;
_toastService.success(
message:
'We\'ve sent a confirmation link to your new email address, click it to verify your new email',
@@ -137,6 +141,7 @@ class OBChangeEmailModalState extends State {
} catch (error) {
_onError(error);
} finally {
+ _requestOperation = null;
_setRequestInProgress(false);
}
}
diff --git a/lib/pages/home/pages/menu/pages/settings/pages/application_settings.dart b/lib/pages/home/pages/menu/pages/settings/pages/application_settings.dart
new file mode 100644
index 000000000..c7b09cc07
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/settings/pages/application_settings.dart
@@ -0,0 +1,28 @@
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/tiles/actions/clear_application_cache_tile.dart';
+import 'package:Openbook/widgets/tiles/actions/clear_application_preferences_tile.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBApplicationSettingsPage extends StatelessWidget {
+ Widget build(BuildContext context) {
+ return CupertinoPageScaffold(
+ backgroundColor: Color.fromARGB(0, 0, 0, 0),
+ navigationBar: OBThemedNavigationBar(title: 'Application settings'),
+ child: OBPrimaryColorContainer(
+ child: ListView(
+ physics: const ClampingScrollPhysics(),
+ // Important: Remove any padding from the ListView.
+ padding: EdgeInsets.zero,
+ children: [
+ OBClearApplicationCacheTile(),
+ OBClearApplicationPreferencesTile(),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/settings/settings.dart b/lib/pages/home/pages/menu/pages/settings/settings.dart
index 2f7db97a8..2c734b76e 100644
--- a/lib/pages/home/pages/menu/pages/settings/settings.dart
+++ b/lib/pages/home/pages/menu/pages/settings/settings.dart
@@ -1,5 +1,3 @@
-import 'package:Openbook/pages/home/pages/menu/pages/settings/modals/change-password/change_password.dart';
-import 'package:Openbook/pages/home/pages/menu/pages/settings/modals/change_email/change_email.dart';
import 'package:Openbook/provider.dart';
import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
@@ -12,7 +10,6 @@ class OBSettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var openbookProvider = OpenbookProvider.of(context);
- var localizationService = openbookProvider.localizationService;
var navigationService = openbookProvider.navigationService;
return CupertinoPageScaffold(
@@ -25,43 +22,21 @@ class OBSettingsPage extends StatelessWidget {
padding: EdgeInsets.zero,
children: [
ListTile(
- leading: const OBIcon(OBIcons.email),
- title: OBText(localizationService.trans('SETTINGS.CHANGE_EMAIL')),
+ leading: const OBIcon(OBIcons.account),
+ title: OBText('Account settings'),
onTap: () {
- Navigator.of(context).push(MaterialPageRoute(
- fullscreenDialog: true,
- builder: (BuildContext context) => Material(
- child: OBChangeEmailModal(),
- )));
- },
- ),
- ListTile(
- leading: const OBIcon(OBIcons.lock),
- title:
- OBText(localizationService.trans('SETTINGS.CHANGE_PASSWORD')),
- onTap: () {
- Navigator.of(context).push(MaterialPageRoute(
- fullscreenDialog: true,
- builder: (BuildContext context) => Material(
- child: OBChangePasswordModal(),
- )));
+ navigationService.navigateToAccountSettingsPage(
+ context: context);
},
),
ListTile(
- leading: const OBIcon(OBIcons.notifications),
- title: OBText('Notifications'),
+ leading: const OBIcon(OBIcons.application),
+ title: OBText('Application settings'),
onTap: () {
- navigationService.navigateToNotificationsSettings(
+ navigationService.navigateToApplicationSettingsPage(
context: context);
},
),
- ListTile(
- leading: const OBIcon(OBIcons.deleteCommunity),
- title: OBText('Delete account'),
- onTap: () {
- navigationService.navigateToDeleteAccount(context: context);
- },
- )
],
),
),
diff --git a/lib/pages/home/pages/menu/pages/themes/themes.dart b/lib/pages/home/pages/menu/pages/themes/themes.dart
new file mode 100644
index 000000000..50f875570
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/themes/themes.dart
@@ -0,0 +1,37 @@
+import 'package:Openbook/models/user.dart';
+import 'package:Openbook/pages/home/lib/poppable_page_controller.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/themes/widgets/curated_themes.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/theming/secondary_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBThemesPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return CupertinoPageScaffold(
+ navigationBar: OBThemedNavigationBar(
+ title: 'Themes',
+ ),
+ child: OBPrimaryColorContainer(
+ child: Column(
+ children: [
+ Expanded(
+ child: ListView(
+ physics: const ClampingScrollPhysics(),
+ // Important: Remove any padding from the ListView.
+ padding: EdgeInsets.zero,
+ children: [
+ OBCuratedThemes()
+ ],
+ )),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/themes/widgets/curated_themes.dart b/lib/pages/home/pages/menu/pages/themes/widgets/curated_themes.dart
new file mode 100644
index 000000000..cfffd0656
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/themes/widgets/curated_themes.dart
@@ -0,0 +1,54 @@
+import 'package:Openbook/models/theme.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/themes/widgets/theme_preview.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:Openbook/services/theme.dart';
+import 'package:flutter/material.dart';
+
+class OBCuratedThemes extends StatelessWidget {
+ ThemeService _themeService;
+
+ @override
+ Widget build(BuildContext context) {
+ _themeService = OpenbookProvider.of(context).themeService;
+
+ return Padding(
+ padding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 10.0),
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ GridView.extent(
+ padding: EdgeInsets.symmetric(vertical: 20),
+ primary: false,
+ physics: const NeverScrollableScrollPhysics(),
+ maxCrossAxisExtent: OBThemePreview.maxWidth,
+ children: _buildThemePreviews(),
+ shrinkWrap: true,
+ ),
+ ],
+ ),
+ ));
+ }
+
+ List _buildThemePreviews() {
+ var themes = _themeService.getCuratedThemes();
+ var res = [];
+
+ for (var theme in themes) {
+ var builder = Builder(builder: (buildContext) {
+ return OBThemePreview(
+ theme,
+ onThemePreviewPressed: (OBTheme theme) {
+ _themeService.setActiveTheme(theme);
+ },
+ );
+ });
+
+ res.add(builder);
+ }
+
+ return res;
+ }
+}
diff --git a/lib/pages/home/pages/menu/widgets/theme_preview.dart b/lib/pages/home/pages/menu/pages/themes/widgets/theme_preview.dart
similarity index 92%
rename from lib/pages/home/pages/menu/widgets/theme_preview.dart
rename to lib/pages/home/pages/menu/pages/themes/widgets/theme_preview.dart
index 4bccc3313..32b6f15cf 100644
--- a/lib/pages/home/pages/menu/widgets/theme_preview.dart
+++ b/lib/pages/home/pages/menu/pages/themes/widgets/theme_preview.dart
@@ -4,6 +4,7 @@ import 'package:Openbook/widgets/theming/text.dart';
import 'package:flutter/material.dart';
class OBThemePreview extends StatelessWidget {
+ static const maxWidth = 120.0;
final OBTheme theme;
final OnThemePreviewPressed onThemePreviewPressed;
@@ -32,7 +33,7 @@ class OBThemePreview extends StatelessWidget {
}
},
child: ConstrainedBox(
- constraints: BoxConstraints(minWidth: 70),
+ constraints: BoxConstraints(minWidth: 70, maxWidth: maxWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -56,6 +57,8 @@ class OBThemePreview extends StatelessWidget {
OBText(
theme.name,
size: OBTextSize.small,
+ maxLines: 3,
+ textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
)
],
diff --git a/lib/pages/home/pages/menu/pages/useful_links.dart b/lib/pages/home/pages/menu/pages/useful_links.dart
new file mode 100644
index 000000000..6c179cdc0
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/useful_links.dart
@@ -0,0 +1,86 @@
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/theming/secondary_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBUsefulLinksPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ var urlLauncherService = openbookProvider.urlLauncherService;
+
+ return CupertinoPageScaffold(
+ navigationBar: OBThemedNavigationBar(
+ title: 'Useful links',
+ ),
+ child: OBPrimaryColorContainer(
+ child: Column(
+ children: [
+ Expanded(
+ child: ListView(
+ physics: const ClampingScrollPhysics(),
+ // Important: Remove any padding from the ListView.
+ padding: EdgeInsets.zero,
+ children: [
+ ListTile(
+ leading: const OBIcon(OBIcons.dashboard ),
+ title: OBText('Github project board'),
+ subtitle: OBSecondaryText(
+ 'Take a look at what we\'re currently working on'),
+ onTap: () {
+ urlLauncherService.launchUrl(
+ 'https://github.com/orgs/OpenbookOrg/projects/3');
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.featureRequest),
+ title: OBText('Feature requests'),
+ subtitle: OBSecondaryText(
+ 'Request a feature or upvote existing requests'),
+ onTap: () {
+ urlLauncherService.launchUrl(
+ 'https://openbook.canny.io/feature-requests');
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.bug),
+ title: OBText('Bug tracker'),
+ subtitle:
+ OBSecondaryText('Report a bug or upvote existing bugs'),
+ onTap: () {
+ urlLauncherService.launchUrl(
+ 'https://openbook.canny.io/bugs');
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.guide),
+ title: OBText('Community guide'),
+ subtitle: OBSecondaryText(
+ 'An introduction to the Openbook Experience by @meep'),
+ onTap: () {
+ urlLauncherService
+ .launchUrl('https://openbook.support/');
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.slackChannel),
+ title: OBText('Community slack channel'),
+ subtitle: OBSecondaryText(
+ 'A place to discuss everything about Openbook'),
+ onTap: () {
+ urlLauncherService.launchUrl(
+ 'https://join.slack.com/t/openbookorg/shared_invite/enQtNDI2NjI3MDM0MzA2LTYwM2E1Y2NhYWRmNTMzZjFhYWZlYmM2YTQ0MWEwYjYyMzcxMGI0MTFhNTIwYjU2ZDI1YjllYzlhOWZjZDc4ZWY');
+ },
+ ),
+ ],
+ )),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/user_invites/pages/user_invite_detail.dart b/lib/pages/home/pages/menu/pages/user_invites/pages/user_invite_detail.dart
new file mode 100644
index 000000000..9dabd366c
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/pages/user_invite_detail.dart
@@ -0,0 +1,146 @@
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/user_invites/widgets/user_invite_detail_header.dart';
+import 'package:Openbook/services/modal_service.dart';
+import 'package:Openbook/services/user_invites_api.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/theming/primary_accent_text.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:Openbook/widgets/theming/secondary_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:share/share.dart';
+
+class OBUserInviteDetailPage extends StatefulWidget {
+ final UserInvite userInvite;
+ final bool showEdit;
+
+ OBUserInviteDetailPage({
+ @required this.userInvite,
+ this.showEdit = false
+ });
+
+ @override
+ State createState() {
+ return OBUserInviteDetailPageState();
+ }
+}
+
+class OBUserInviteDetailPageState extends State {
+ UserService _userService;
+ ToastService _toastService;
+ ModalService _modalService;
+ UserInvitesApiService _userInvitesApiService;
+ bool _needsBootstrap;
+
+ @override
+ void initState() {
+ super.initState();
+ _needsBootstrap = true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var provider = OpenbookProvider.of(context);
+ _userService = provider.userService;
+ _toastService = provider.toastService;
+ _modalService = provider.modalService;
+ _userInvitesApiService = provider.userInvitesApiService;
+
+ if (_needsBootstrap) {
+ _needsBootstrap = false;
+ }
+
+ return OBCupertinoPageScaffold(
+ backgroundColor: Color.fromARGB(0, 0, 0, 0),
+ navigationBar: OBThemedNavigationBar(
+ title: 'Invite',
+ trailing: _buildNavigationBarTrailingItem(),
+ ),
+ child: OBPrimaryColorContainer(
+ child: SizedBox(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ OBUserInviteDetailHeader(widget.userInvite),
+ Expanded(
+ child: ListView(
+ physics: const ClampingScrollPhysics(),
+ padding: EdgeInsets.zero,
+ children: _buildActionsList())
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ List _buildActionsList() {
+ if (widget.userInvite.createdUser != null) {
+ return [
+ const SizedBox()
+ ];
+ }
+
+ return [
+ ListTile(
+ leading: const OBIcon(OBIcons.chat),
+ title: const OBText('Share invite yourself'),
+ subtitle: const OBSecondaryText(
+ 'Choose from messaging apps, etc.',
+ ),
+ onTap: () {
+ String apiURL = _userInvitesApiService.apiURL;
+ String token = widget.userInvite.token;
+ Share.share(UserInvite.getShareMessageForInviteWithToken(token, apiURL));
+ },
+ ),
+ ListTile(
+ leading: const OBIcon(OBIcons.email),
+ title: const OBText('Share invite by email'),
+ subtitle: const OBSecondaryText(
+ 'We will send an invitation email with instructions on your behalf',
+ ),
+ onTap: () async {
+ await _modalService.openSendUserInviteEmail(
+ context: context,
+ userInvite: widget.userInvite);
+ Navigator.of(context).pop();
+ },
+ )
+ ];
+ }
+
+
+ Widget _buildNavigationBarTrailingItem() {
+ if (!widget.showEdit) return const SizedBox();
+ return GestureDetector(
+ onTap: () {
+ _modalService.openEditUserInvite(
+ userInvite: widget.userInvite,
+ context: context);
+ },
+ child: OBPrimaryAccentText('Edit'),
+ );
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart b/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart
new file mode 100644
index 000000000..73361b36d
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/user_invites.dart
@@ -0,0 +1,269 @@
+import 'package:Openbook/models/user.dart';
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/models/user_invites_list.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/user_invites/widgets/user_invite_count.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/user_invites/widgets/user_invite_tile.dart';
+import 'package:Openbook/services/modal_service.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/icon_button.dart';
+import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
+import 'package:Openbook/widgets/page_scaffold.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/theming/primary_color_container.dart';
+import 'package:async/async.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:Openbook/services/httpie.dart';
+
+class OBUserInvitesPage extends StatefulWidget {
+ @override
+ State createState() {
+ return OBUserInvitesPageState();
+ }
+}
+
+class OBUserInvitesPageState extends State {
+ UserService _userService;
+ ToastService _toastService;
+ ModalService _modalService;
+
+ User _user;
+ GlobalKey _refreshIndicatorKey;
+ ScrollController _userInvitesScrollController;
+ List _userInvites = [];
+ List _userInvitesSearchResults = [];
+ OBMyInvitesGroupController _acceptedInvitesGroupController;
+ OBMyInvitesGroupController _pendingInvitesGroupController;
+
+ CancelableOperation _refreshUserOperation;
+
+ bool _needsBootstrap;
+
+ @override
+ void initState() {
+ super.initState();
+ _userInvitesScrollController = ScrollController();
+ _refreshIndicatorKey = GlobalKey();
+ _acceptedInvitesGroupController = OBMyInvitesGroupController();
+ _pendingInvitesGroupController = OBMyInvitesGroupController();
+ _needsBootstrap = true;
+ _userInvites = [];
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ if (_refreshUserOperation != null) _refreshUserOperation.cancel();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_needsBootstrap) {
+ var provider = OpenbookProvider.of(context);
+ _userService = provider.userService;
+ _toastService = provider.toastService;
+ _modalService = provider.modalService;
+ _user = _userService.getLoggedInUser();
+ _bootstrap();
+ _needsBootstrap = false;
+ }
+
+ return OBCupertinoPageScaffold(
+ backgroundColor: Color.fromARGB(0, 0, 0, 0),
+ navigationBar: OBThemedNavigationBar(
+ title: 'My Invites',
+ trailing: Opacity(
+ opacity: _user.inviteCount == 0 ? 0.5 : 1.0,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ OBUserInviteCount(
+ count: _user.inviteCount,
+ ),
+ const SizedBox(
+ width: 10,
+ ),
+ OBIconButton(
+ OBIcons.add,
+ themeColor: OBIconThemeColor.primaryAccent,
+ onPressed: _onWantsToCreateInvite,
+ ),
+ ],
+ ),
+ ),
+ ),
+ child: Stack(
+ children: [
+ OBPrimaryColorContainer(
+ child: Column(
+ children: [
+ Expanded(
+ child: RefreshIndicator(
+ key: _refreshIndicatorKey,
+ onRefresh: _refreshInvites,
+ child: ListView(
+ key: Key('myUserInvites'),
+ controller: _userInvitesScrollController,
+ // Need always scrollable for pull to refresh to work
+ physics: const AlwaysScrollableScrollPhysics(),
+ padding: EdgeInsets.all(0),
+ children: [
+ Column(
+ children: [
+ OBMyInvitesGroup(
+ key: Key('AcceptedInvitesGroup'),
+ controller: _acceptedInvitesGroupController,
+ title: 'Accepted',
+ groupName: 'accepted invites',
+ groupItemName: 'accepted invite',
+ maxGroupListPreviewItems: 5,
+ inviteListSearcher: _searchAcceptedUserInvites,
+ inviteGroupListItemDeleteCallback: _onUserInviteDeletedCallback,
+ inviteGroupListRefresher: _refreshAcceptedInvites,
+ inviteGroupListOnScrollLoader: _loadMoreAcceptedInvites,
+ ),
+ OBMyInvitesGroup(
+ key: Key('PendingInvitesGroup'),
+ controller: _pendingInvitesGroupController,
+ title: 'Pending',
+ groupName: 'pending invites',
+ groupItemName: 'pending invite',
+ maxGroupListPreviewItems: 5,
+ inviteListSearcher: _searchPendingUserInvites,
+ inviteGroupListItemDeleteCallback: _onUserInviteDeletedCallback,
+ inviteGroupListRefresher: _refreshPendingInvites,
+ inviteGroupListOnScrollLoader: _loadMorePendingInvites),
+ ],
+ )
+ ]),
+ ),
+ ),
+ ],
+ )),
+ ],
+ ));
+ }
+
+ Widget _onUserInviteDeletedCallback(BuildContext context, UserInvite userInvite) {
+ _refreshUser();
+ }
+
+ void _bootstrap() async {
+ await _refreshInvites();
+ }
+
+ Future _refreshInvites() async {
+ try {
+ await Future.wait([
+ _refreshUser(),
+ _acceptedInvitesGroupController.refresh(),
+ _pendingInvitesGroupController.refresh()
+ ]);
+ _scrollToTop();
+ } catch (error) {
+ _onError(error);
+ }
+ }
+
+ Future _refreshUser() async {
+ if (_refreshUserOperation != null) _refreshUserOperation.cancel();
+ _refreshUserOperation = CancelableOperation.fromFuture(_userService.refreshUser());
+ await _refreshUserOperation.value;
+ User refreshedUser = _userService.getLoggedInUser();
+ setState(() {
+ _user = refreshedUser;
+ });
+ }
+
+ Future> _refreshPendingInvites() async {
+ UserInvitesList pendingInvitesList = await _userService.getUserInvites(status: UserInviteFilterByStatus.pending);
+ return pendingInvitesList.invites;
+ }
+
+ Future> _refreshAcceptedInvites() async {
+ UserInvitesList acceptedInvitesList = await _userService.getUserInvites(status: UserInviteFilterByStatus.accepted);
+ return acceptedInvitesList.invites;
+ }
+
+ Future> _loadMorePendingInvites(
+ List currentInviteList) async {
+ int offset = currentInviteList.length;
+
+ UserInvitesList morePendingInvites = await _userService.getUserInvites(offset: offset, status: UserInviteFilterByStatus.pending);
+ return morePendingInvites.invites;
+ }
+
+ Future> _loadMoreAcceptedInvites(
+ List currentInviteList) async {
+ int offset = currentInviteList.length;
+
+ UserInvitesList moreAcceptedInvites = await _userService.getUserInvites(offset: offset, status: UserInviteFilterByStatus.accepted);
+ return moreAcceptedInvites.invites;
+ }
+
+ Future> _searchPendingUserInvites(String query) async {
+ UserInvitesList results = await _userService.searchUserInvites(query: query, status: UserInviteFilterByStatus.pending);
+ return results.invites;
+ }
+
+ Future> _searchAcceptedUserInvites(String query) async {
+ UserInvitesList results = await _userService.searchUserInvites(query: query, status: UserInviteFilterByStatus.accepted);
+ return results.invites;
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _onWantsToCreateInvite() async {
+ if (_user.inviteCount == 0) {
+ _showNoInvitesLeft();
+ return;
+ }
+ UserInvite createdUserInvite =
+ await _modalService.openCreateUserInvite(context: context);
+ if (createdUserInvite != null) {
+ _onUserInviteCreated(createdUserInvite);
+ }
+ }
+
+ void _showNoInvitesLeft() {
+ _toastService.error(message: 'You have no invites left', context: context);
+ }
+
+ void _removeUserInvite(UserInvite userInvite) {
+ setState(() {
+ _userInvites.remove(userInvite);
+ _userInvitesSearchResults.remove(userInvite);
+ });
+ }
+
+ void _onUserInviteCreated(UserInvite createdUserInvite) {
+ _refreshInvites();
+ _scrollToTop();
+ }
+
+ void _scrollToTop() {
+ _userInvitesScrollController.animateTo(
+ 0.0,
+ curve: Curves.easeOut,
+ duration: const Duration(milliseconds: 300),
+ );
+ }
+}
+
+typedef Future OnWantsToCreateUserInvite();
+typedef Future OnWantsToEditUserInvite(UserInvite userInvite);
+typedef void OnWantsToSeeUserInvite(UserInvite userInvite);
diff --git a/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart b/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart
new file mode 100644
index 000000000..44a76bdac
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/widgets/my_invite_group.dart
@@ -0,0 +1,268 @@
+import 'package:Openbook/libs/str_utils.dart';
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/user_invites/widgets/user_invite_tile.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:Openbook/services/navigation_service.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/widgets/http_list.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/theming/secondary_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:async/async.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBMyInvitesGroup extends StatefulWidget {
+ final OBHttpListRefresher inviteGroupListRefresher;
+ final OBHttpListSearcher inviteListSearcher;
+ final void Function(BuildContext, UserInvite) inviteGroupListItemDeleteCallback;
+ final OBHttpListOnScrollLoader inviteGroupListOnScrollLoader;
+ final OBMyInvitesGroupFallbackBuilder noGroupItemsFallbackBuilder;
+ final OBMyInvitesGroupController controller;
+ final String groupItemName;
+ final String groupName;
+ final int maxGroupListPreviewItems;
+ final String title;
+
+ const OBMyInvitesGroup({
+ Key key,
+ @required this.inviteGroupListRefresher,
+ @required this.inviteGroupListOnScrollLoader,
+ @required this.groupItemName,
+ @required this.inviteListSearcher,
+ @required this.groupName,
+ @required this.title,
+ @required this.maxGroupListPreviewItems,
+ @required this.inviteGroupListItemDeleteCallback,
+ this.noGroupItemsFallbackBuilder,
+ this.controller,
+ }) : super(key: key);
+
+ @override
+ OBMyInvitesGroupState createState() {
+ return OBMyInvitesGroupState();
+ }
+}
+
+class OBMyInvitesGroupState extends State {
+ bool _needsBootstrap;
+ ToastService _toastService;
+ NavigationService _navigationService;
+ List _inviteGroupList;
+ bool _refreshInProgress;
+ CancelableOperation _refreshOperation;
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.controller != null) widget.controller.attach(this);
+ _needsBootstrap = true;
+ _inviteGroupList = [];
+ _refreshInProgress = false;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_needsBootstrap) {
+ var openbookProvider = OpenbookProvider.of(context);
+ _toastService = openbookProvider.toastService;
+ _navigationService = openbookProvider.navigationService;
+ _bootstrap();
+ _needsBootstrap = false;
+ }
+
+ int listItemCount =
+ _inviteGroupList.length < widget.maxGroupListPreviewItems
+ ? _inviteGroupList.length
+ : widget.maxGroupListPreviewItems;
+
+ if (listItemCount == 0) {
+ if (widget.noGroupItemsFallbackBuilder != null && !_refreshInProgress) {
+ return widget.noGroupItemsFallbackBuilder(
+ context, _refreshInvites);
+ }
+ return const SizedBox();
+ }
+
+ List columnItems = [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
+ child: OBText(
+ widget.title,
+ style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
+ ),
+ ),
+ ListView.separated(
+ key: Key(widget.groupName + 'invitesGroup'),
+ physics: const NeverScrollableScrollPhysics(),
+ separatorBuilder: _buildInviteSeparator,
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
+ shrinkWrap: true,
+ itemCount: listItemCount,
+ itemBuilder: _buildGroupListPreviewItem),
+ ];
+
+ if (_inviteGroupList.length > widget.maxGroupListPreviewItems) {
+ columnItems.add(_buildSeeAllButton());
+ }
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: columnItems,
+ );
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ if (widget.controller != null) widget.controller.detach();
+ if (_refreshOperation != null) _refreshOperation.cancel();
+ }
+
+ Widget _buildSeeAllButton() {
+ return GestureDetector(
+ onTap: _onWantsToSeeAll,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ OBSecondaryText(
+ 'See all ' + widget.groupName,
+ style: TextStyle(fontSize: 16),
+ ),
+ const SizedBox(
+ width: 5,
+ ),
+ OBIcon(OBIcons.seeMore, themeColor: OBIconThemeColor.secondaryText)
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildGroupListPreviewItem(BuildContext context, index) {
+ UserInvite userInvite = _inviteGroupList[index];
+ return _buildInviteTile(context, userInvite);
+ }
+
+ Widget _buildInviteTile(BuildContext context, UserInvite userInvite) {
+ var onUserInviteDeletedCallback = () {
+ _removeUserInvite(userInvite);
+ widget.inviteGroupListItemDeleteCallback(context, userInvite);
+ };
+
+ return OBUserInviteTile(
+ userInvite: userInvite,
+ onUserInviteDeletedCallback: onUserInviteDeletedCallback,
+ );
+ }
+
+ Widget _buildInviteSeparator(BuildContext context, int index) {
+ return const SizedBox();
+ }
+
+ void _bootstrap() {
+ _refreshInvites();
+ }
+
+ void _removeUserInvite(UserInvite userInvite) {
+ setState(() {
+ _inviteGroupList.remove(userInvite);
+ });
+ }
+
+ Future _refreshInvites() async {
+ if (_refreshOperation != null) _refreshOperation.cancel();
+ _setRefreshInProgress(true);
+ try {
+ _refreshOperation =
+ CancelableOperation.fromFuture(widget.inviteGroupListRefresher());
+
+ List groupInvites = await _refreshOperation.value;
+
+ _setUserInviteGroupList(groupInvites);
+ } catch (error) {
+ _onError(error);
+ } finally {
+ _refreshOperation = null;
+ _setRefreshInProgress(false);
+ }
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _onWantsToSeeAll() {
+ _navigationService.navigateToBlankPageWithWidget(
+ context: context,
+ key: Key('obMyUserInvitesGroup' + widget.groupItemName),
+ navBarTitle: capitalize(widget.groupName),
+ widget: _buildSeeAllGroupItemsPage());
+ }
+
+ Widget _buildSeeAllGroupItemsPage() {
+ return Column(
+ children: [
+ Expanded(
+ child: OBHttpList(
+ separatorBuilder: _buildInviteSeparator,
+ listSearcher: widget.inviteListSearcher,
+ searchResultListItemBuilder: _buildInviteTile,
+ listItemBuilder: _buildInviteTile,
+ listRefresher: widget.inviteGroupListRefresher,
+ listOnScrollLoader: widget.inviteGroupListOnScrollLoader,
+ resourcePluralName: widget.groupName,
+ resourceSingularName: widget.groupItemName
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _setUserInviteGroupList(List invites) {
+ setState(() {
+ _inviteGroupList = invites;
+ });
+ }
+
+ void _setRefreshInProgress(bool refreshInProgress) {
+ setState(() {
+ _refreshInProgress = refreshInProgress;
+ });
+ }
+}
+
+class OBMyInvitesGroupController {
+ OBMyInvitesGroupState _state;
+
+ void attach(OBMyInvitesGroupState state) {
+ this._state = state;
+ }
+
+ void detach() {
+ this._state = null;
+ }
+
+ Future refresh() {
+ if (_state == null) return Future.value();
+ return _state._refreshInvites();
+ }
+}
+
+typedef Future OBMyInvitesGroupRetry();
+
+typedef Widget OBMyInvitesGroupFallbackBuilder(
+ BuildContext context, OBMyInvitesGroupRetry retry);
diff --git a/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_count.dart b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_count.dart
new file mode 100644
index 000000000..dc40270d2
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_count.dart
@@ -0,0 +1,53 @@
+import 'package:Openbook/models/theme.dart';
+import 'package:Openbook/provider.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class OBUserInviteCount extends StatelessWidget {
+ final int count;
+ final double size;
+
+ const OBUserInviteCount({Key key, this.count, this.size=19}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ var openbookProvider = OpenbookProvider.of(context);
+ var toastService = openbookProvider.toastService;
+ var themeService = openbookProvider.themeService;
+ var themeValueParserService = openbookProvider.themeValueParserService;
+
+ return StreamBuilder(
+ stream: themeService.themeChange,
+ initialData: themeService.getActiveTheme(),
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ var theme = snapshot.data;
+ var primaryAccentColor =
+ themeValueParserService.parseGradient(theme.primaryAccentColor);
+ return GestureDetector(
+ onTap: () {
+ if (count != 1) {
+ toastService.info(message: 'You have $count invites left', context: context);
+ } else {
+ toastService.info(message: 'You have $count invite left', context: context);
+ }
+ },
+ child: Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ gradient: primaryAccentColor,
+ borderRadius: BorderRadius.circular(50)),
+ child: Center(
+ child: count != null ? Text(
+ count.toString(),
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: count < 10 ? 12 : 10,
+ fontWeight: FontWeight.bold),
+ ) : const SizedBox()
+ ),
+ ),
+ );
+ });
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_detail_header.dart b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_detail_header.dart
new file mode 100644
index 000000000..fca31c30b
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_detail_header.dart
@@ -0,0 +1,66 @@
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/pages/home/pages/menu/pages/user_invites/widgets/user_invite_nickname.dart';
+import 'package:Openbook/widgets/theming/actionable_smart_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:flutter/material.dart';
+
+class OBUserInviteDetailHeader extends StatelessWidget {
+ final UserInvite userInvite;
+
+ OBUserInviteDetailHeader(this.userInvite);
+
+ @override
+ Widget build(BuildContext context) {
+
+ return StreamBuilder(
+ stream: this.userInvite.updateSubject,
+ initialData: this.userInvite,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ var userInvite = snapshot.data;
+
+ List columnItems = [_buildUserInviteNickname(userInvite)];
+
+ columnItems.add(_buildUserDescription(userInvite));
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: columnItems,
+ );
+ });
+ }
+
+ Widget _buildUserInviteNickname(UserInvite userInvite) {
+
+ return Padding(
+ padding: EdgeInsets.only(left: 20.0, right: 20, top: 20.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Flexible(
+ child: OBUserInviteNickname(userInvite),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildUserDescription(UserInvite userInvite) {
+ Widget _description;
+ if (userInvite.createdUser != null) {
+ _description = OBActionableSmartText(text: 'Joined with username @${userInvite.createdUser.username}');
+ } else if (userInvite.isInviteEmailSent) {
+ _description = OBText('Pending, invite email sent to ${userInvite.email}');
+ } else {
+ _description = OBText('Pending');
+ }
+
+ return Padding(
+ padding: EdgeInsets.only(left: 20.0, right: 20, top: 10.0, bottom: 20),
+ child: Column(
+ children: [
+ _description,
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_nickname.dart b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_nickname.dart
new file mode 100644
index 000000000..5b0779d83
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_nickname.dart
@@ -0,0 +1,44 @@
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/widgets/theming/primary_accent_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:Openbook/widgets/theming/secondary_text.dart';
+import 'package:flutter/material.dart';
+
+class OBUserInviteNickname extends StatelessWidget {
+ final UserInvite userInvite;
+
+ OBUserInviteNickname(this.userInvite);
+
+ @override
+ Widget build(BuildContext context) {
+ return StreamBuilder(
+ stream: userInvite.updateSubject,
+ initialData: userInvite,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ var userInvite = snapshot.data;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ OBSecondaryText(
+ 'Nickname',
+ ),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Flexible(
+ child: OBPrimaryAccentText(
+ userInvite.nickname,
+ size: OBTextSize.extraLarge,
+ overflow: TextOverflow.ellipsis,
+ maxLines: 2,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ )
+ ],
+ )
+ ],
+ );
+ });
+ }
+}
diff --git a/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_tile.dart b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_tile.dart
new file mode 100644
index 000000000..1bfabb01f
--- /dev/null
+++ b/lib/pages/home/pages/menu/pages/user_invites/widgets/user_invite_tile.dart
@@ -0,0 +1,151 @@
+import 'package:Openbook/models/user_invite.dart';
+import 'package:Openbook/provider.dart';
+import 'package:Openbook/services/theme.dart';
+import 'package:Openbook/services/toast.dart';
+import 'package:Openbook/services/user.dart';
+import 'package:Openbook/widgets/emoji_picker/widgets/emoji_groups/widgets/emoji_group/widgets/emoji.dart';
+import 'package:Openbook/widgets/icon.dart';
+import 'package:Openbook/widgets/theming/actionable_smart_text.dart';
+import 'package:Openbook/widgets/theming/text.dart';
+import 'package:Openbook/widgets/theming/secondary_text.dart';
+import 'package:async/async.dart';
+import 'package:flutter/material.dart';
+import 'package:Openbook/services/httpie.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+
+class OBUserInviteTile extends StatefulWidget {
+ final UserInvite userInvite;
+ final VoidCallback onUserInviteDeletedCallback;
+
+ OBUserInviteTile(
+ {@required this.userInvite, Key key, this.onUserInviteDeletedCallback})
+ : super(key: key);
+
+ @override
+ State createState() {
+ return OBUserInviteTileState();
+ }
+}
+
+class OBUserInviteTileState extends State {
+ bool _requestInProgress;
+ UserService _userService;
+ ToastService _toastService;
+
+ CancelableOperation _deleteOperation;
+
+ @override
+ void initState() {
+ super.initState();
+ _requestInProgress = false;
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ if (_deleteOperation != null)
+ _deleteOperation.cancel();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var provider = OpenbookProvider.of(context);
+ _userService = provider.userService;
+ _toastService = provider.toastService;
+ var navigationService = provider.navigationService;
+ Widget tile;
+
+ if (widget.userInvite.createdUser != null) {
+ tile = ListTile(
+ onTap: () {
+ navigationService.navigateToUserProfile(
+ user: widget.userInvite.createdUser,
+ context: context);
+ },
+ leading: const OBIcon(OBIcons.invite),
+ title: OBText(
+ widget.userInvite.nickname,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ subtitle: _buildActionableSecondaryText()
+ );
+ } else {
+ tile = Slidable(
+ delegate: new SlidableDrawerDelegate(),
+ actionExtentRatio: 0.25,
+ child: ListTile(
+ onTap: () {
+ navigationService.navigateToInviteDetailPage(
+ userInvite: widget.userInvite,
+ context: context
+ );
+ },
+ leading: const OBIcon(OBIcons.invite),
+ title: OBText(
+ widget.userInvite.nickname,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ subtitle: _buildActionableSecondaryText()),
+ secondaryActions: [
+ new IconSlideAction(
+ caption: 'Delete',
+ color: Colors.red,
+ icon: Icons.delete,
+ onTap: _deleteUserInvite),
+ ],
+ );
+ }
+
+ if (_requestInProgress) {
+ tile = Opacity(opacity: 0.5, child: tile);
+ }
+ return tile;
+ }
+
+ Widget _buildActionableSecondaryText() {
+ if (widget.userInvite.createdUser != null) {
+ return OBActionableSmartText(
+ size: OBTextSize.mediumSecondary,
+ text: 'Joined with username @${widget.userInvite.createdUser.username}',
+ );
+ } else {
+ return OBSecondaryText('Pending');
+ }
+ }
+
+ void _deleteUserInvite() async {
+ _setRequestInProgress(true);
+ try {
+ _deleteOperation = CancelableOperation.fromFuture(_userService.deleteUserInvite(widget.userInvite));
+ await _deleteOperation.value;
+ _setRequestInProgress(false);
+ if (widget.onUserInviteDeletedCallback != null) {
+ widget.onUserInviteDeletedCallback();
+ }
+ } catch (error) {
+ _onError(error);
+ } finally {
+ _setRequestInProgress(false);
+ _deleteOperation = null;
+ }
+ }
+
+ void _onError(error) async {
+ if (error is HttpieConnectionRefusedError) {
+ _toastService.error(
+ message: error.toHumanReadableMessage(), context: context);
+ } else if (error is HttpieRequestError) {
+ String errorMessage = await error.toHumanReadableMessage();
+ _toastService.error(message: errorMessage, context: context);
+ } else {
+ _toastService.error(message: 'Unknown error', context: context);
+ throw error;
+ }
+ }
+
+ void _setRequestInProgress(bool requestInProgress) {
+ setState(() {
+ _requestInProgress = requestInProgress;
+ });
+ }
+}
diff --git a/lib/pages/home/pages/menu/widgets/curated_themes.dart b/lib/pages/home/pages/menu/widgets/curated_themes.dart
deleted file mode 100644
index b41b5b5a7..000000000
--- a/lib/pages/home/pages/menu/widgets/curated_themes.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-import 'package:Openbook/models/theme.dart';
-import 'package:Openbook/pages/home/pages/menu/widgets/theme_preview.dart';
-import 'package:Openbook/provider.dart';
-import 'package:Openbook/widgets/theming/text.dart';
-import 'package:flutter/material.dart';
-
-class OBCuratedThemes extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- // TODO: implement build
-
- var themeService = OpenbookProvider.of(context).themeService;
- var themes = themeService.getCuratedThemes();
-
- return Padding(
- padding: EdgeInsets.only(left: 20.0, right: 20.0, bottom: 10.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.only(bottom: 15.0),
- child: const OBText(
- 'Curated themes',
- style: TextStyle(fontWeight: FontWeight.bold),
- size: OBTextSize.large,
- ),
- ),
- SizedBox(
- height: 70,
- child: ListView.separated(
- physics: const ClampingScrollPhysics(),
- scrollDirection: Axis.horizontal,
- itemCount: themes.length,
- itemBuilder: (BuildContext context, int index) {
- return OBThemePreview(
- themes[index],
- onThemePreviewPressed: (OBTheme theme) {
- themeService.setActiveTheme(theme);
- },
- );
- },
- separatorBuilder: (BuildContext context, int index) {
- return const SizedBox(
- width: 20,
- );
- },
- ),
- )
- ],
- ),
- );
- }
-}
diff --git a/lib/pages/home/pages/post_comments/post.dart b/lib/pages/home/pages/post_comments/post.dart
index 3c5e0304f..4f2d6feb0 100644
--- a/lib/pages/home/pages/post_comments/post.dart
+++ b/lib/pages/home/pages/post_comments/post.dart
@@ -5,7 +5,6 @@ import 'package:Openbook/pages/home/pages/post_comments/widgets/post_comment/pos
import 'package:Openbook/services/theme.dart';
import 'package:Openbook/services/theme_value_parser.dart';
import 'package:Openbook/services/user_preferences.dart';
-import 'package:Openbook/widgets/icon.dart';
import 'package:Openbook/widgets/nav_bars/themed_nav_bar.dart';
import 'package:Openbook/widgets/page_scaffold.dart';
import 'package:Openbook/provider.dart';
@@ -14,9 +13,10 @@ import 'package:Openbook/services/user.dart';
import 'package:Openbook/widgets/theming/primary_color_container.dart';
import 'package:Openbook/widgets/theming/secondary_text.dart';
import 'package:Openbook/widgets/theming/text.dart';
+import 'package:async/async.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
-import 'package:loadmore/loadmore.dart';
+import 'package:Openbook/widgets/load_more.dart';
import 'package:Openbook/services/httpie.dart';
class OBPostCommentsPage extends StatefulWidget {
@@ -49,6 +49,11 @@ class OBPostCommentsPageState extends State {
FocusNode _commentInputFocusNode;
PostCommentsSortType _currentSort;
+ CancelableOperation _refreshCommentsOperation;
+ CancelableOperation _refreshPostOperation;
+ CancelableOperation _loadMoreBottomCommentsOperation;
+ CancelableOperation _toggleSortCommentsOperation;
+
@override
void initState() {
super.initState();
@@ -61,6 +66,17 @@ class OBPostCommentsPageState extends State {
_commentInputFocusNode = FocusNode();
}
+ @override
+ void dispose() {
+ super.dispose();
+ if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel();
+ if (_loadMoreBottomCommentsOperation != null)
+ _loadMoreBottomCommentsOperation.cancel();
+ if (_refreshPostOperation != null) _refreshPostOperation.cancel();
+ if (_toggleSortCommentsOperation != null)
+ _toggleSortCommentsOperation.cancel();
+ }
+
@override
Widget build(BuildContext context) {
if (_needsBootstrap) {
@@ -115,12 +131,12 @@ class OBPostCommentsPageState extends State {
_removePostCommentAtIndex(commentIndex);
};
- return OBExpandedPostComment(
- postComment: postComment,
- post: widget.post,
- onPostCommentDeletedCallback:
- onPostCommentDeletedCallback,
- );
+ return OBPostComment(
+ key: Key('postComment#${postComment.id}'),
+ postComment: postComment,
+ post: widget.post,
+ onPostCommentDeletedCallback:
+ onPostCommentDeletedCallback);
}),
onLoadMore: _loadMoreBottomComments),
),
@@ -131,6 +147,7 @@ class OBPostCommentsPageState extends State {
autofocus: widget.autofocusCommentInput,
commentTextFieldFocusNode: _commentInputFocusNode,
onPostCommentCreated: _onPostCommentCreated,
+ onPostCommentWillBeCreated: _onPostCommentWillBeCreated,
)
],
),
@@ -191,6 +208,8 @@ class OBPostCommentsPageState extends State {
}
void _onWantsToToggleSortComments() async {
+ if (_toggleSortCommentsOperation != null)
+ _toggleSortCommentsOperation.cancel();
PostCommentsSortType newSortType;
if (_currentSort == PostCommentsSortType.asc) {
@@ -200,9 +219,10 @@ class OBPostCommentsPageState extends State {
}
try {
- _postComments = (await _userService.getCommentsForPost(widget.post,
- sort: newSortType))
- .comments;
+ _toggleSortCommentsOperation = CancelableOperation.fromFuture(
+ _userService.getCommentsForPost(widget.post, sort: newSortType));
+
+ _postComments = (await _toggleSortCommentsOperation.value).comments;
_setCurrentSortValue(newSortType);
_userPreferencesService.setPostCommentsSortType(newSortType);
_setPostComments(_postComments);
@@ -210,32 +230,44 @@ class OBPostCommentsPageState extends State {
_setNoMoreItemsToLoad(false);
} catch (error) {
_onError(error);
+ } finally {
+ _toggleSortCommentsOperation = null;
}
}
Future _refreshComments() async {
+ if (_refreshCommentsOperation != null) _refreshCommentsOperation.cancel();
try {
- _postComments = (await _userService.getCommentsForPost(widget.post,
- sort: _currentSort))
- .comments;
+ _refreshCommentsOperation = CancelableOperation.fromFuture(
+ _userService.getCommentsForPost(widget.post, sort: _currentSort));
+ _postComments = (await _refreshCommentsOperation.value).comments;
_setPostComments(_postComments);
_scrollToTop();
_setNoMoreItemsToLoad(false);
} catch (error) {
_onError(error);
+ } finally {
+ _refreshCommentsOperation = null;
}
}
Future _refreshPost() async {
+ if (_refreshPostOperation != null) _refreshPostOperation.cancel();
try {
// This will trigger the updateSubject of the post
- await _userService.getPostWithUuid(widget.post.uuid);
+ _refreshPostOperation = CancelableOperation.fromFuture(
+ _userService.getPostWithUuid(widget.post.uuid));
+ await _refreshPostOperation.value;
} catch (error) {
_onError(error);
+ } finally {
+ _refreshPostOperation = null;
}
}
Future _loadMoreBottomComments() async {
+ if (_loadMoreBottomCommentsOperation != null)
+ _loadMoreBottomCommentsOperation.cancel();
if (_postComments.length == 0) return true;
var lastPost = _postComments.last;
@@ -245,16 +277,16 @@ class OBPostCommentsPageState extends State {
var moreComments;
if (_currentSort == PostCommentsSortType.dec) {
- moreComments = (await _userService.getCommentsForPost(
- widget.post,
- maxId: lastPostId))
- .comments;
+ _loadMoreBottomCommentsOperation = CancelableOperation.fromFuture(
+ _userService.getCommentsForPost(widget.post, maxId: lastPostId));
} else {
- moreComments = (await _userService.getCommentsForPost(widget.post,
- minId: lastPostId + 1, sort: _currentSort))
- .comments;
+ _loadMoreBottomCommentsOperation = CancelableOperation.fromFuture(
+ _userService.getCommentsForPost(widget.post,
+ minId: lastPostId + 1, sort: _currentSort));
}
+ moreComments = (await _loadMoreBottomCommentsOperation.value).comments;
+
if (moreComments.length == 0) {
_setNoMoreItemsToLoad(true);
} else {
@@ -263,6 +295,8 @@ class OBPostCommentsPageState extends State