diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd92e9a9..d0054b2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,60 +1,135 @@
-## 0.1.35
+## 0.2.1-alpha+11
-- Add backgroundColor as part of StreamChatThemeData
+- Update llc dependency
-## 0.1.34
+## 0.2.1-alpha+10
-- Add `onUserAvatarTap`
+- Update llc dependency
-## 0.1.33
+## 0.2.1-alpha+9
-- Add default user and channel image to `StreamChatTheme`
+- Add read indicators
+- Update llc dependency
-## 0.1.32
+## 0.2.1-alpha+8
-- Update llc version
+- User queryMembers for mentions
-## 0.1.31
+## 0.2.1-alpha+7
-- Add `initialMessage` property to `MessageInput`
+- Update llc dependency
-## 0.1.30
+## 0.2.1-alpha+6
-- Add simple rendering of file attachments
+- Update llc dependency
+- Minor bugfix
-## 0.1.29
+## 0.2.1-alpha+4
-- Add `doImageUploadRequest` and `doFileUploadRequest` to `MessageInput` to let users customize file uploading (eg: custom cdn)
+- Update llc dependency
-## 0.1.28
+- Add system messages
-- Add `onTap` to `ChannelImage`
+## 0.2.1-alpha+3
-- Add `onImageTap` and `onTitleTap` to `ChannelHeader`
+- Update llc dependency
-- Add `onImageTap` to `ChannelListView`
+- Fix hero tag generation for attachment
-## 0.1.27
+## 0.2.1-alpha+2
-- Show other member user's name and image in one to one channels
+- Fixed reactions bubble going below other messages
+- Updated llc dependency
-## 0.1.25
+## 0.2.1-alpha+1
+
+- Removed the additional `Navigator` in `StreamChat` widget.
+ It was added to make the app have the `StreamChat` widget as ancestor in every route.
+ Now the recommended way to add `StreamChat` to your app is using the `builder` property of your `MaterialApp` widget.
+ Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to every route of your app.
+ Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information.
+
+```dart
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ theme: ThemeData.light(),
+ darkTheme: ThemeData.dark(),
+ themeMode: ThemeMode.system,
+ builder: (context, widget) {
+ return StreamChat(
+ child: widget,
+ client: client,
+ );
+ },
+ home: ChannelListPage(),
+ );
+```
+
+- Fix reaction bubble going below previous message on iOS
+
+- Fix message list view reloading messages even if the pagination is ended
+
+## 0.2.1-alpha
+
+- New message widget
+- Moved some properties from `MessageListView` to `MessageWidget`
+- Added `MessageDetails` property to `MessageBuilder`
+- Added example to customize the message using `MessageWidget` (`customize_message_widget.dart`)
+
+## 0.2.0-alpha+15
+
+- Add background color in StreamChatTheme
+
+## 0.2.0-alpha+13
+
+- Handle channel deleted event
+
+## 0.2.0-alpha+11
+
+- Fix message builder and add messageList to it
+
+## 0.2.0-alpha+10
+
+- Add date divider builder
+
+- Fix reply indicator tap
+
+## 0.2.0-alpha+9
+
+- Add `attachmentBuilders` to `MessageWidget` and `MessageListView`
+
+## 0.2.0-alpha+7
+
+- Update llc dependency
+
+## 0.2.0-alpha+5
+
+- Remove dependencies on notification service
+
+- Expose some helping method for integrate offline storage with push notifications
+
+## 0.2.0-alpha+3
- Fix overflow in mentions overlay
-## 0.1.23
+## 0.2.0-alpha+2
-- Hotfix
+- Add better mime detection
-## 0.1.22
-
-- Better mime type detection
-
-## 0.1.21
+## 0.2.0-alpha+1
- Fix video loading and error
-## 0.1.20
+## 0.2.0-alpha
+
+- Offline storage
+
+- Push notifications
+
+- Minor bug fixes
+
+## 0.1.20s
- Add message configuration properties to MessageListView
diff --git a/README.md b/README.md
index dc828ad6..9049b4c0 100644
--- a/README.md
+++ b/README.md
@@ -33,14 +33,14 @@ The example is available under the [example](https://github.com/GetStream/stream
```yaml
dependencies:
- stream_chat_flutter: ^0.1.21
+ stream_chat_flutter: ^0.1.19
```
You should then run `flutter packages get`
### Alpha version
-Use version `^0.2.0-alpha+1` to use the latest available version.
+Use version `^0.2.0-alpha+2` to use the latest available version.
Note that this is still an alpha version. There may be some bugs, and the API can change in breaking ways.
@@ -65,10 +65,11 @@ Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check
### Business logic components
-We provide 2 Widgets dedicated to business logic and state management:
+We provide 3 Widgets dedicated to business logic and state management:
- [StreamChat](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChat-class.html)
- [StreamChannel](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChannel-class.html)
+- [ChannelsBloc](https://pub.dev/documentation/stream_chat_flutter/0.2.0-alpha+2/stream_chat_flutter/ChannelsBloc-class.html)
### UI Components
@@ -156,6 +157,12 @@ Out of the box, all chat widgets use their default styling, and there are two wa
}
}
```
+
+### Offline storage
+
+By default the library saves information about channels and messages in a SQLite DB.
+
+Set the property `persistenceEnabled` to false if you don't want to use the offline storage.
## Contributing
diff --git a/analysis_options.yaml b/analysis_options.yaml
index fedac90d..3723c0af 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -3,7 +3,7 @@ include: package:pedantic/analysis_options.yaml
analyzer:
exclude:
- lib/**/*.g.dart
- - example/*
+ - example/**
linter:
rules:
@@ -44,7 +44,7 @@ linter:
- package_prefixed_library_names
- prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
-# - public_member_api_docs
+ - public_member_api_docs
- slash_for_doc_comments
# - sort_constructors_first
# - sort_unnamed_constructors_first
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 0f6a5e54..dda667cf 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -64,4 +64,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+ implementation 'com.google.firebase:firebase-messaging:20.1.2'
}
+
+apply plugin: 'com.google.gms.google-services'
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 1b2ad157..c5f1e85b 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -7,7 +7,7 @@
FlutterApplication and put your custom class here. -->
+
+
+
+
diff --git a/example/android/app/src/main/kotlin/com/example/example/Application.kt b/example/android/app/src/main/kotlin/com/example/example/Application.kt
new file mode 100644
index 00000000..dd47a03c
--- /dev/null
+++ b/example/android/app/src/main/kotlin/com/example/example/Application.kt
@@ -0,0 +1,27 @@
+package com.example.example
+
+import com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin
+import io.flutter.app.FlutterApplication
+import io.flutter.plugin.common.PluginRegistry
+import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback
+import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin
+import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService
+import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin
+import io.flutter.plugins.pathprovider.PathProviderPlugin
+
+class Application : FlutterApplication(), PluginRegistrantCallback {
+ override fun onCreate() {
+ super.onCreate()
+ FlutterFirebaseMessagingService.setPluginRegistrant(this)
+ }
+
+ override fun registerWith(registry: PluginRegistry?) {
+ PathProviderPlugin.registerWith(registry?.registrarFor(
+ "io.flutter.plugins.pathprovider.PathProviderPlugin"))
+ SharedPreferencesPlugin.registerWith(registry?.registrarFor(
+ "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"))
+ FlutterLocalNotificationsPlugin.registerWith(registry?.registrarFor(
+ "com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin"))
+ FirebaseMessagingPlugin.registerWith(registry?.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"))
+ }
+}
\ No newline at end of file
diff --git a/example/android/build.gradle b/example/android/build.gradle
index 3100ad2d..70b3637d 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.google.gms:google-services:4.3.2'
}
}
diff --git a/example/ios/Notifications/Info.plist b/example/ios/Notifications/Info.plist
new file mode 100644
index 00000000..a225b5ca
--- /dev/null
+++ b/example/ios/Notifications/Info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Notifications
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.usernotifications.service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NotificationService
+
+
+
diff --git a/example/ios/Notifications/NotificationService.swift b/example/ios/Notifications/NotificationService.swift
new file mode 100644
index 00000000..7d085536
--- /dev/null
+++ b/example/ios/Notifications/NotificationService.swift
@@ -0,0 +1,182 @@
+//
+// NotificationService.swift
+// Notifications
+//
+// Created by Salvatore Giordano on 25/03/2020.
+// Copyright © 2020 The Chromium Authors. All rights reserved.
+//
+
+import UserNotifications
+import StreamChatClient
+
+final class NotificationService: UNNotificationServiceExtension {
+
+ var contentHandler: ((UNNotificationContent) -> Void)?
+ var bestAttemptContent: UNMutableNotificationContent?
+
+ override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ self.contentHandler = contentHandler
+ bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
+
+ guard let sharedDefaults = UserDefaults(suiteName: "group.io.stream.flutter"),
+ let apiKey = sharedDefaults.string(forKey: "KEY_API_KEY"),
+ let userId = sharedDefaults.string(forKey: "KEY_USER_ID"),
+ let token = sharedDefaults.string(forKey: "KEY_TOKEN"),
+ let messageId = bestAttemptContent?.userInfo["message_id"] as? String else {
+ return
+ }
+
+ Client.config = .init(apiKey: apiKey, logOptions: .error)
+ Client.shared.set(user: User(id: userId), token: token) { res in
+ guard res.isConnected else {
+ return
+ }
+
+ Client.shared.message(withId: messageId) { [weak self] res in
+ if let message = res.value?.message,
+ let channel = res.value?.channel {
+ let messageWrapper = MessageWrapper(channel: channel, message: message)
+ if let encodedData = try? JSONEncoder.stream.encode(messageWrapper),
+ let encodedString = String(data: encodedData, encoding: .utf8) {
+ let storedMessages = sharedDefaults.stringArray(forKey: "messageQueue") ?? []
+ sharedDefaults.setValue(storedMessages + [encodedString], forKey: "messageQueue")
+
+ // Modify the notification content here...
+ self?.bestAttemptContent?.title = "[modified] \(self?.bestAttemptContent?.title ?? "")"
+ contentHandler(self?.bestAttemptContent ?? request.content)
+ }
+ Client.shared.disconnect()
+ }
+ }
+ }
+ }
+
+ override func serviceExtensionTimeWillExpire() {
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
+ contentHandler(bestAttemptContent)
+ }
+ }
+}
+
+public struct MessageWrapper: Encodable {
+ private enum CodingKeys: String, CodingKey {
+ case id
+ case channel
+ case type
+ case user
+ case created = "created_at"
+ case updated = "updated_at"
+ case text
+ case command
+ case args
+ case attachments
+ case parentId = "parent_id"
+ case showReplyInChannel = "show_in_channel"
+ case mentionedUsers = "mentioned_users"
+ }
+
+ init(channel: Channel, message: Message) {
+ id = message.id
+ type = message.type
+ user = message.user
+ created = message.created
+ updated = message.updated
+ text = message.text
+ command = message.command
+ args = message.args
+ attachments = message.attachments
+ parentId = message.parentId
+ showReplyInChannel = message.showReplyInChannel
+ mentionedUsers = message.mentionedUsers
+ extraData = message.extraData
+ self.channel = ChannelWrapper(channel: channel)
+ }
+
+ /// A message id.
+ public let id: String
+ /// The channel cid.
+ public let channel: ChannelWrapper?
+ /// A message type (see `MessageType`).
+ public let type: MessageType
+ /// A user (see `User`).
+ public let user: User
+ /// A created date.
+ public let created: Date
+ /// A updated date.
+ public let updated: Date
+ /// A text.
+ public let text: String
+ /// A used command name.
+ public let command: String?
+ /// A used command args.
+ public let args: String?
+ /// Attachments (see `Attachment`).
+ public let attachments: [Attachment]
+ /// A parent message id.
+ public let parentId: String?
+ /// Check if this reply message needs to show in the channel.
+ public let showReplyInChannel: Bool
+ /// Mentioned users (see `User`).
+ public let mentionedUsers: [User]
+ /// An extra data for the message.
+ public let extraData: Codable?
+}
+
+public struct ChannelWrapper: Encodable {
+ /// Coding keys for the encoding.
+ private enum CodingKeys: String, CodingKey {
+ case id
+ case cid
+ case type
+ case name
+ case imageURL = "image"
+ case members
+ case lastMessageDate = "last_message_at"
+ case createdBy = "created_by"
+ case created = "created_at"
+ case deleted = "deleted_at"
+ case frozen
+ }
+
+ init(channel: Channel) {
+ id = channel.id
+ cid = channel.cid
+ type = channel.type
+ name = channel.name
+ imageURL = channel.imageURL
+ lastMessageDate = channel.lastMessageDate
+ created = channel.created
+ deleted = channel.deleted
+ createdBy = channel.createdBy
+ config = channel.config
+ frozen = channel.frozen
+ extraData = channel.extraData
+ }
+
+ /// A channel id.
+ public let id: String
+ /// A channel type + id.
+ public let cid: ChannelId
+ /// A channel type.
+ public let type: ChannelType
+ /// A channel name.
+ public let name: String?
+ /// An image of the channel.
+ public let imageURL: URL?
+ /// The last message date.
+ public let lastMessageDate: Date?
+ /// A channel created date.
+ public let created: Date
+ /// A channel deleted date.
+ public let deleted: Date?
+ /// A creator of the channel.
+ public let createdBy: User?
+ /// A config.
+ public let config: Channel.Config
+ /// Checks if the channel is frozen.
+ public let frozen: Bool
+ /// A list of user ids of the channel members.
+ public let members = Set()
+ /// An extra data for the channel.
+ public let extraData: Codable?
+}
diff --git a/example/ios/Notifications/Notifications.entitlements b/example/ios/Notifications/Notifications.entitlements
new file mode 100644
index 00000000..00390120
--- /dev/null
+++ b/example/ios/Notifications/Notifications.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.io.stream.flutter
+
+
+
diff --git a/example/ios/Podfile b/example/ios/Podfile
index b30a428b..ad84eb7b 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '9.0'
+platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -63,7 +63,7 @@ target 'Runner' do
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
-
+ pod 'StreamChatClient'
# Plugin Pods
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 66ae94b1..a3821dbf 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,23 +1,168 @@
PODS:
+ - DKImagePickerController/Core (4.2.2):
+ - DKImagePickerController/ImageDataManager
+ - DKImagePickerController/Resource
+ - DKImagePickerController/ImageDataManager (4.2.2)
+ - DKImagePickerController/PhotoGallery (4.2.2):
+ - DKImagePickerController/Core
+ - DKPhotoGallery
+ - DKImagePickerController/Resource (4.2.2)
+ - DKPhotoGallery (0.0.14):
+ - DKPhotoGallery/Core (= 0.0.14)
+ - DKPhotoGallery/Model (= 0.0.14)
+ - DKPhotoGallery/Preview (= 0.0.14)
+ - DKPhotoGallery/Resource (= 0.0.14)
+ - SDWebImage
+ - SDWebImageFLPlugin
+ - DKPhotoGallery/Core (0.0.14):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Preview
+ - SDWebImage
+ - SDWebImageFLPlugin
+ - DKPhotoGallery/Model (0.0.14):
+ - SDWebImage
+ - SDWebImageFLPlugin
+ - DKPhotoGallery/Preview (0.0.14):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Resource
+ - SDWebImage
+ - SDWebImageFLPlugin
+ - DKPhotoGallery/Resource (0.0.14):
+ - SDWebImage
+ - SDWebImageFLPlugin
- file_picker (0.0.1):
+ - DKImagePickerController/PhotoGallery
- Flutter
+ - Firebase/Core (6.20.0):
+ - Firebase/CoreOnly
+ - FirebaseAnalytics (= 6.3.1)
+ - Firebase/CoreOnly (6.20.0):
+ - FirebaseCore (= 6.6.4)
+ - Firebase/Messaging (6.20.0):
+ - Firebase/CoreOnly
+ - FirebaseMessaging (~> 4.3.0)
+ - firebase_messaging (0.0.1):
+ - Firebase/Core
+ - Firebase/Messaging
+ - Flutter
+ - FirebaseAnalytics (6.3.1):
+ - FirebaseCore (~> 6.6)
+ - FirebaseInstallations (~> 1.1)
+ - GoogleAppMeasurement (= 6.3.1)
+ - GoogleUtilities/AppDelegateSwizzler (~> 6.0)
+ - GoogleUtilities/MethodSwizzler (~> 6.0)
+ - GoogleUtilities/Network (~> 6.0)
+ - "GoogleUtilities/NSData+zlib (~> 6.0)"
+ - nanopb (= 0.3.9011)
+ - FirebaseAnalyticsInterop (1.5.0)
+ - FirebaseCore (6.6.4):
+ - FirebaseCoreDiagnostics (~> 1.2)
+ - FirebaseCoreDiagnosticsInterop (~> 1.2)
+ - GoogleUtilities/Environment (~> 6.5)
+ - GoogleUtilities/Logger (~> 6.5)
+ - FirebaseCoreDiagnostics (1.2.2):
+ - FirebaseCoreDiagnosticsInterop (~> 1.2)
+ - GoogleDataTransportCCTSupport (~> 2.0)
+ - GoogleUtilities/Environment (~> 6.5)
+ - GoogleUtilities/Logger (~> 6.5)
+ - nanopb (~> 0.3.901)
+ - FirebaseCoreDiagnosticsInterop (1.2.0)
+ - FirebaseInstallations (1.1.0):
+ - FirebaseCore (~> 6.6)
+ - GoogleUtilities/UserDefaults (~> 6.5)
+ - PromisesObjC (~> 1.2)
+ - FirebaseInstanceID (4.3.2):
+ - FirebaseCore (~> 6.6)
+ - FirebaseInstallations (~> 1.0)
+ - GoogleUtilities/Environment (~> 6.5)
+ - GoogleUtilities/UserDefaults (~> 6.5)
+ - FirebaseMessaging (4.3.0):
+ - FirebaseAnalyticsInterop (~> 1.5)
+ - FirebaseCore (~> 6.6)
+ - FirebaseInstanceID (~> 4.3)
+ - GoogleUtilities/AppDelegateSwizzler (~> 6.5)
+ - GoogleUtilities/Environment (~> 6.5)
+ - GoogleUtilities/Reachability (~> 6.5)
+ - GoogleUtilities/UserDefaults (~> 6.5)
+ - Protobuf (>= 3.9.2, ~> 3.9)
+ - FLAnimatedImage (1.0.12)
- Flutter (1.0.0)
+ - flutter_apns (0.0.1):
+ - Flutter
- flutter_keyboard_visibility (0.7.0):
- Flutter
+ - flutter_local_notifications (0.0.1):
+ - Flutter
- flutter_plugin_android_lifecycle (0.0.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
+ - GoogleAppMeasurement (6.3.1):
+ - GoogleUtilities/AppDelegateSwizzler (~> 6.0)
+ - GoogleUtilities/MethodSwizzler (~> 6.0)
+ - GoogleUtilities/Network (~> 6.0)
+ - "GoogleUtilities/NSData+zlib (~> 6.0)"
+ - nanopb (= 0.3.9011)
+ - GoogleDataTransport (5.0.0)
+ - GoogleDataTransportCCTSupport (2.0.0):
+ - GoogleDataTransport (~> 5.0)
+ - nanopb (~> 0.3.901)
+ - GoogleUtilities/AppDelegateSwizzler (6.5.2):
+ - GoogleUtilities/Environment
+ - GoogleUtilities/Logger
+ - GoogleUtilities/Network
+ - GoogleUtilities/Environment (6.5.2)
+ - GoogleUtilities/Logger (6.5.2):
+ - GoogleUtilities/Environment
+ - GoogleUtilities/MethodSwizzler (6.5.2):
+ - GoogleUtilities/Logger
+ - GoogleUtilities/Network (6.5.2):
+ - GoogleUtilities/Logger
+ - "GoogleUtilities/NSData+zlib"
+ - GoogleUtilities/Reachability
+ - "GoogleUtilities/NSData+zlib (6.5.2)"
+ - GoogleUtilities/Reachability (6.5.2):
+ - GoogleUtilities/Logger
+ - GoogleUtilities/UserDefaults (6.5.2):
+ - GoogleUtilities/Logger
+ - GzipSwift (5.1.1)
- image_picker (0.0.1):
- Flutter
+ - moor_ffi (0.0.1):
+ - Flutter
+ - nanopb (0.3.9011):
+ - nanopb/decode (= 0.3.9011)
+ - nanopb/encode (= 0.3.9011)
+ - nanopb/decode (0.3.9011)
+ - nanopb/encode (0.3.9011)
- path_provider (0.0.1):
- Flutter
- path_provider_macos (0.0.1):
- Flutter
+ - PromisesObjC (1.2.8)
+ - Protobuf (3.11.4)
+ - ReachabilitySwift (5.0.0)
+ - SDWebImage (5.8.0):
+ - SDWebImage/Core (= 5.8.0)
+ - SDWebImage/Core (5.8.0)
+ - SDWebImageFLPlugin (0.4.0):
+ - FLAnimatedImage (>= 1.0.11)
+ - SDWebImage/Core (~> 5.6)
+ - shared_preferences (0.0.1):
+ - Flutter
+ - shared_preferences_macos (0.0.1):
+ - Flutter
+ - shared_preferences_web (0.0.1):
+ - Flutter
- sqflite (0.0.1):
- Flutter
- FMDB (~> 2.7.2)
+ - Starscream (3.1.1)
+ - StreamChatClient (2.0.1):
+ - GzipSwift (~> 5.1)
+ - ReachabilitySwift (~> 5.0)
+ - Starscream (~> 3.1)
- url_launcher (0.0.1):
- Flutter
- url_launcher_macos (0.0.1):
@@ -33,13 +178,21 @@ PODS:
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
+ - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
+ - flutter_apns (from `.symlinks/plugins/flutter_apns/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
+ - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
+ - moor_ffi (from `.symlinks/plugins/moor_ffi/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`)
+ - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
+ - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`)
+ - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
+ - StreamChatClient
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
- url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`)
- url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`)
@@ -49,23 +202,62 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
+ - DKImagePickerController
+ - DKPhotoGallery
+ - Firebase
+ - FirebaseAnalytics
+ - FirebaseAnalyticsInterop
+ - FirebaseCore
+ - FirebaseCoreDiagnostics
+ - FirebaseCoreDiagnosticsInterop
+ - FirebaseInstallations
+ - FirebaseInstanceID
+ - FirebaseMessaging
+ - FLAnimatedImage
- FMDB
+ - GoogleAppMeasurement
+ - GoogleDataTransport
+ - GoogleDataTransportCCTSupport
+ - GoogleUtilities
+ - GzipSwift
+ - nanopb
+ - PromisesObjC
+ - Protobuf
+ - ReachabilitySwift
+ - SDWebImage
+ - SDWebImageFLPlugin
+ - Starscream
+ - StreamChatClient
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
+ firebase_messaging:
+ :path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
+ flutter_apns:
+ :path: ".symlinks/plugins/flutter_apns/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
+ flutter_local_notifications:
+ :path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_plugin_android_lifecycle:
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
+ moor_ffi:
+ :path: ".symlinks/plugins/moor_ffi/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
path_provider_macos:
:path: ".symlinks/plugins/path_provider_macos/ios"
+ shared_preferences:
+ :path: ".symlinks/plugins/shared_preferences/ios"
+ shared_preferences_macos:
+ :path: ".symlinks/plugins/shared_preferences_macos/ios"
+ shared_preferences_web:
+ :path: ".symlinks/plugins/shared_preferences_web/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
url_launcher:
@@ -82,22 +274,54 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock/ios"
SPEC CHECKSUMS:
- file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245
+ DKImagePickerController: 4a3e7948a848c4348e600b3fe5ce41478835fa10
+ DKPhotoGallery: 0290d32343574f06eaa4c26f8f2f8a1035e916be
+ file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
+ Firebase: fe7f74012742ab403451dd283e6909b8f1fb348a
+ firebase_messaging: 21344b3b3a7d9d325d63a70e3750c0c798fe1e03
+ FirebaseAnalytics: 572e467f3d977825266e8ccd52674aa3e6f47eac
+ FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae
+ FirebaseCore: ed0a24c758a57c2b88c5efa8e6a8195e868af589
+ FirebaseCoreDiagnostics: e9b4cd8ba60dee0f2d13347332e4b7898cca5b61
+ FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850
+ FirebaseInstallations: 575cd32f2aec0feeb0e44f5d0110a09e5e60b47b
+ FirebaseInstanceID: 7ee0d6777013bb952f377b41965bf132b6a075be
+ FirebaseMessaging: 4ec33842d36b3319e062e51fb8b35a74f726950d
+ FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
+ flutter_apns: f516b118e423fe7c0a38771180549c4d6cb67c2f
flutter_keyboard_visibility: 6195387fb6d8f46e5cd6dda4a4154e41f800f545
- flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b
+ flutter_local_notifications: 9e4738ce2471c5af910d961a6b7eadcf57c50186
+ flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
- image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f
- path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
+ GoogleAppMeasurement: c29d405ff76e18551b5d158eaba6753fda8c7542
+ GoogleDataTransport: a857c6a002d201b524dd4bc2ed7e7355ed07e785
+ GoogleDataTransportCCTSupport: 32f75fbe904c82772fcbb6b6bd4525bfb6f2a862
+ GoogleUtilities: ad0f3b691c67909d03a3327cc205222ab8f42e0e
+ GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
+ image_picker: 66aa71bc96850a90590a35d4c4a2907b0d823109
+ moor_ffi: d66c9470c18e9cb333423bbcb493c105c6c774c6
+ nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd
+ path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
+ PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6
+ Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7
+ ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
+ SDWebImage: 84000f962cbfa70c07f19d2234cbfcf5d779b5dc
+ SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8
+ shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
+ shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087
+ shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9
sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0
- url_launcher: a1c0cc845906122c4784c542523d8cacbded5626
+ Starscream: 4bb2f9942274833f7b4d296a55504dcfc7edb7b0
+ StreamChatClient: 91b0f585e7dc92ade58e657daffafb16d485b2a6
+ url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313
url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c
- video_player: 69c5f029fac4ffe4fc8a85ea7f7b793709661549
+ video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7
wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4
-PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83
+PODFILE CHECKSUM: 5cc7e2f1316491ee530029e2e8391f4100d41fbd
COCOAPODS: 1.8.4
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index 42d1671f..456c5e1f 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -7,28 +7,45 @@
objects = {
/* Begin PBXBuildFile section */
+ 0BC14C50242B5A7A0028DE94 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC14C4F242B5A7A0028DE94 /* NotificationService.swift */; };
+ 0BC14C54242B5A7A0028DE94 /* Notifications.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0BC14C4D242B5A7A0028DE94 /* Notifications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
- 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7DEC2743BD66C91B700A3B97 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BB2E5E4E236267EDF0D8817 /* Pods_Runner.framework */; };
- 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
- 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ 0BC14C52242B5A7A0028DE94 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 0BC14C4C242B5A7A0028DE94;
+ remoteInfo = Notifications;
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXCopyFilesBuildPhase section */
+ 0BC14C55242B5A7A0028DE94 /* Embed App Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 0BC14C54242B5A7A0028DE94 /* Notifications.appex in Embed App Extensions */,
+ );
+ name = "Embed App Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
- 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
- 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@@ -36,11 +53,15 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 0BC14C4D242B5A7A0028DE94 /* Notifications.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Notifications.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0BC14C4F242B5A7A0028DE94 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ 0BC14C51242B5A7A0028DE94 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 0BC14C5A242B5ED90028DE94 /* Notifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notifications.entitlements; sourceTree = ""; };
+ 0BC14C5B242B5FF50028DE94 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
2452A9E77396497EB4CF3072 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
- 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
68F846A6DB42D92393F5F7E0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
@@ -49,7 +70,6 @@
8BB2E5E4E236267EDF0D8817 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
- 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
@@ -58,12 +78,17 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 0BC14C4A242B5A7A0028DE94 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
- 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
7DEC2743BD66C91B700A3B97 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -71,12 +96,20 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 0BC14C4E242B5A7A0028DE94 /* Notifications */ = {
+ isa = PBXGroup;
+ children = (
+ 0BC14C5A242B5ED90028DE94 /* Notifications.entitlements */,
+ 0BC14C4F242B5A7A0028DE94 /* NotificationService.swift */,
+ 0BC14C51242B5A7A0028DE94 /* Info.plist */,
+ );
+ path = Notifications;
+ sourceTree = "";
+ };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
- 3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
- 9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
@@ -87,8 +120,9 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
- 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 0BC14C4E242B5A7A0028DE94 /* Notifications */,
97C146EF1CF9000F007C117D /* Products */,
CF168B61BAB91958681C7C21 /* Pods */,
BC09A38346C8B2CD72199469 /* Frameworks */,
@@ -99,6 +133,7 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
+ 0BC14C4D242B5A7A0028DE94 /* Notifications.appex */,
);
name = Products;
sourceTree = "";
@@ -106,6 +141,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ 0BC14C5B242B5FF50028DE94 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -147,6 +183,23 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
+ 0BC14C4C242B5A7A0028DE94 /* Notifications */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0BC14C59242B5A7A0028DE94 /* Build configuration list for PBXNativeTarget "Notifications" */;
+ buildPhases = (
+ 0BC14C49242B5A7A0028DE94 /* Sources */,
+ 0BC14C4A242B5A7A0028DE94 /* Frameworks */,
+ 0BC14C4B242B5A7A0028DE94 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Notifications;
+ productName = Notifications;
+ productReference = 0BC14C4D242B5A7A0028DE94 /* Notifications.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -159,10 +212,12 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5702861DACEDB848A3E454E8 /* [CP] Embed Pods Frameworks */,
+ 0BC14C55242B5A7A0028DE94 /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
+ 0BC14C53242B5A7A0028DE94 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@@ -175,13 +230,20 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
+ LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
+ 0BC14C4C242B5A7A0028DE94 = {
+ CreatedOnToolsVersion = 11.4;
+ DevelopmentTeam = EHV7XZLAHA;
+ ProvisioningStyle = Manual;
+ };
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
DevelopmentTeam = EHV7XZLAHA;
LastSwiftMigration = 1100;
+ ProvisioningStyle = Manual;
};
};
};
@@ -199,11 +261,19 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
+ 0BC14C4C242B5A7A0028DE94 /* Notifications */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
+ 0BC14C4B242B5A7A0028DE94 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -230,7 +300,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
5702861DACEDB848A3E454E8 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
@@ -286,6 +356,14 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 0BC14C49242B5A7A0028DE94 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0BC14C50242B5A7A0028DE94 /* NotificationService.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -297,6 +375,14 @@
};
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ 0BC14C53242B5A7A0028DE94 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 0BC14C4C242B5A7A0028DE94 /* Notifications */;
+ targetProxy = 0BC14C52242B5A7A0028DE94 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -317,6 +403,90 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 0BC14C56242B5A7A0028DE94 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = Notifications/Notifications.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = EHV7XZLAHA;
+ ENABLE_BITCODE = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ INFOPLIST_FILE = Notifications/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.Notifications;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "flutter example notifications";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 0BC14C57242B5A7A0028DE94 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = Notifications/Notifications.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = EHV7XZLAHA;
+ ENABLE_BITCODE = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ INFOPLIST_FILE = Notifications/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.Notifications;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "flutter example notifications";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 0BC14C58242B5A7A0028DE94 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = Notifications/Notifications.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = EHV7XZLAHA;
+ ENABLE_BITCODE = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ INFOPLIST_FILE = Notifications/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.Notifications;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "flutter example notifications";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Profile;
+ };
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
@@ -372,8 +542,12 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
@@ -382,13 +556,15 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = io.stream.flutter;
+ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "flutter app example";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -507,8 +683,12 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
@@ -517,13 +697,15 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = io.stream.flutter;
+ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "flutter app example";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -535,8 +717,12 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
@@ -545,13 +731,15 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = io.stream.flutter;
+ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "flutter app example";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -561,6 +749,16 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ 0BC14C59242B5A7A0028DE94 /* Build configuration list for PBXNativeTarget "Notifications" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0BC14C56242B5A7A0028DE94 /* Debug */,
+ 0BC14C57242B5A7A0028DE94 /* Release */,
+ 0BC14C58242B5A7A0028DE94 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
index 70693e4a..3f549155 100644
--- a/example/ios/Runner/AppDelegate.swift
+++ b/example/ios/Runner/AppDelegate.swift
@@ -3,11 +3,39 @@ import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
+ let sharedDefaults = UserDefaults(suiteName: "group.io.stream.flutter")
+
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ if let messageQueue = sharedDefaults?.stringArray(forKey: "messageQueue") {
+ UserDefaults.standard.setValue(messageQueue, forKey: "flutter.messageQueue")
+ sharedDefaults?.removeObject(forKey: "messageQueue")
+ }
+
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ override func applicationDidEnterBackground(_ application: UIApplication) {
+ if let apiKey = UserDefaults.standard.string(forKey: "flutter.KEY_API_KEY") {
+ sharedDefaults?.setValue(apiKey, forKey: "KEY_API_KEY")
+ }
+
+ if let token = UserDefaults.standard.string(forKey: "flutter.KEY_TOKEN") {
+ sharedDefaults?.setValue(token, forKey: "KEY_TOKEN")
+ }
+
+ if let userId = UserDefaults.standard.string(forKey: "flutter.KEY_USER_ID") {
+ sharedDefaults?.setValue(userId, forKey: "KEY_USER_ID")
+ }
+ }
+
+ override func applicationWillEnterForeground(_ application: UIApplication) {
+ if let messageQueue = sharedDefaults?.stringArray(forKey: "messageQueue") {
+ UserDefaults.standard.setValue(messageQueue, forKey: "flutter.messageQueue")
+ sharedDefaults?.removeObject(forKey: "messageQueue")
+ }
+ }
}
diff --git a/example/ios/Runner/Runner.entitlements b/example/ios/Runner/Runner.entitlements
new file mode 100644
index 00000000..967ba7f2
--- /dev/null
+++ b/example/ios/Runner/Runner.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ aps-environment
+ development
+ com.apple.security.application-groups
+
+ group.io.stream.flutter
+
+
+
diff --git a/example/lib/custom_message.dart b/example/lib/custom_message.dart
index 8d9e726a..5da4541a 100644
--- a/example/lib/custom_message.dart
+++ b/example/lib/custom_message.dart
@@ -16,13 +16,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// or to retrieve outer scope needed such as messages from the [Channel.state].
void main() async {
final client = Client(
- 'b67pax5b2wdq',
+ 's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
- User(id: 'falling-mountain-7'),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
runApp(MyApp(client));
@@ -36,12 +36,11 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
- home: Container(
- child: StreamChat(
- client: client,
- child: ChannelListPage(),
- ),
+ builder: (context, child) => StreamChat(
+ child: child,
+ client: client,
),
+ home: ChannelListPage(),
);
}
}
@@ -50,17 +49,19 @@ class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: ChannelListView(
- filter: {
- 'members': {
- '\$in': [StreamChat.of(context).user.id],
- }
- },
- sort: [SortOption('last_message_at')],
- pagination: PaginationParams(
- limit: 20,
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ }
+ },
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
),
- channelWidget: ChannelPage(),
),
);
}
@@ -88,7 +89,12 @@ class ChannelPage extends StatelessWidget {
);
}
- Widget _messageBuilder(context, message, index) {
+ Widget _messageBuilder(
+ BuildContext context,
+ MessageDetails details,
+ List messages,
+ ) {
+ final message = details.message;
final isCurrentUser = StreamChat.of(context).user.id == message.user.id;
final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
diff --git a/example/lib/custom_theme.dart b/example/lib/custom_theme.dart
index 078faeb8..21db1c6c 100644
--- a/example/lib/custom_theme.dart
+++ b/example/lib/custom_theme.dart
@@ -20,13 +20,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// You can perform these more granular style changes using [StreamChatTheme.copyWith].
void main() async {
final client = Client(
- 'b67pax5b2wdq',
+ 's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
- User(id: 'falling-mountain-7'),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
runApp(MyApp(client));
@@ -45,23 +45,22 @@ class MyApp extends StatelessWidget {
return MaterialApp(
theme: theme,
- home: Container(
- child: StreamChat(
- streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
- ownMessageTheme: MessageTheme(
- messageBackgroundColor: Colors.black,
- messageText: TextStyle(
- color: Colors.white,
- ),
- avatarTheme: AvatarTheme(
- borderRadius: BorderRadius.circular(8),
- ),
+ builder: (context, child) => StreamChat(
+ child: child,
+ client: client,
+ streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
+ ownMessageTheme: MessageTheme(
+ messageBackgroundColor: Colors.black,
+ messageText: TextStyle(
+ color: Colors.white,
+ ),
+ avatarTheme: AvatarTheme(
+ borderRadius: BorderRadius.circular(8),
),
),
- client: client,
- child: ChannelListPage(),
),
),
+ home: ChannelListPage(),
);
}
}
@@ -70,17 +69,19 @@ class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: ChannelListView(
- filter: {
- 'members': {
- '\$in': [StreamChat.of(context).user.id],
- }
- },
- sort: [SortOption('last_message_at')],
- pagination: PaginationParams(
- limit: 20,
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ }
+ },
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
),
- channelWidget: ChannelPage(),
),
);
}
diff --git a/example/lib/customize_channel_preview.dart b/example/lib/customize_channel_preview.dart
index 949624d1..55da346c 100644
--- a/example/lib/customize_channel_preview.dart
+++ b/example/lib/customize_channel_preview.dart
@@ -21,13 +21,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// - We retrieve the count of unread messages from [Channel.state]
void main() async {
final client = Client(
- 'b67pax5b2wdq',
+ 's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
- User(id: 'falling-mountain-7'),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
runApp(MyApp(client));
@@ -41,12 +41,11 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
- home: Container(
- child: StreamChat(
- client: client,
- child: ChannelListPage(),
- ),
+ builder: (context, child) => StreamChat(
+ child: child,
+ client: client,
),
+ home: ChannelListPage(),
);
}
}
@@ -55,29 +54,31 @@ class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: ChannelListView(
- filter: {
- 'members': {
- '\$in': [StreamChat.of(context).user.id],
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ },
},
- },
- channelPreviewBuilder: _channelPreviewBuilder,
- sort: [SortOption('last_message_at')],
- pagination: PaginationParams(
- limit: 20,
+ channelPreviewBuilder: _channelPreviewBuilder,
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
),
- channelWidget: ChannelPage(),
),
);
}
Widget _channelPreviewBuilder(BuildContext context, Channel channel) {
final lastMessage = channel.state.messages.reversed.firstWhere(
- (message) => message.type != "deleted",
+ (message) => message.type != 'deleted',
orElse: () => null,
);
- final subtitle = (lastMessage == null ? "nothing yet" : lastMessage.text);
+ final subtitle = (lastMessage == null ? 'nothing yet' : lastMessage.text);
final opacity = channel.state.unreadCount > .0 ? 1.0 : 0.5;
return ListTile(
diff --git a/example/lib/customize_message_widget.dart b/example/lib/customize_message_widget.dart
new file mode 100644
index 00000000..fc614125
--- /dev/null
+++ b/example/lib/customize_message_widget.dart
@@ -0,0 +1,133 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+/// Fifth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
+///
+/// Customizing how messages are rendered is another very common use-case that the SDK supports easily.
+///
+/// Replace the built-in message component with your own is done by passing it as a builder function to the [MessageListView] widget.
+///
+/// The message builder function will get the usual [BuildContext] argument as well as the [Message] object and its position inside the list.
+///
+/// If you look at the code you can see that we use [StreamChat.of] to retrieve the current user so that we can style messages own messages in a different way.
+///
+/// Since custom widgets and builders are always children of [StreamChat] or part of a [Channel],
+/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly
+/// or to retrieve outer scope needed such as messages from the [Channel.state].
+void main() async {
+ final client = Client(
+ 's2dxdhpxd94g',
+ logLevel: Level.INFO,
+ );
+
+ await client.setUser(
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
+ );
+
+ runApp(MyApp(client));
+}
+
+class MyApp extends StatelessWidget {
+ final Client client;
+
+ MyApp(this.client);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ builder: (context, child) => StreamChat(
+ child: child,
+ client: client,
+ ),
+ home: Container(
+ child: ChannelListPage(),
+ ),
+ );
+ }
+}
+
+class ChannelListPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ }
+ },
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
+ ),
+ ),
+ );
+ }
+}
+
+class ChannelPage extends StatelessWidget {
+ const ChannelPage({
+ Key key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: ChannelHeader(),
+ body: Column(
+ children: [
+ Expanded(
+ child: MessageListView(
+ messageBuilder: _messageBuilder,
+ ),
+ ),
+ MessageInput(),
+ ],
+ ),
+ );
+ }
+
+ Widget _messageBuilder(
+ BuildContext context,
+ MessageDetails details,
+ List messages,
+ ) {
+ final message = details.message;
+ final color = details.isMyMessage ? Colors.blueGrey : Colors.blue;
+ if (message.isSystem) {
+ return SizedBox();
+ }
+ return MessageWidget(
+ message: message,
+ messageTheme: details.isMyMessage
+ ? StreamChatTheme.of(context).ownMessageTheme
+ : StreamChatTheme.of(context).otherMessageTheme,
+ borderSide: BorderSide(
+ color: color,
+ width: 2,
+ ),
+ padding: const EdgeInsets.all(2),
+ attachmentBorderSide: BorderSide(
+ color: color,
+ width: 2,
+ ),
+ attachmentPadding: EdgeInsets.all(8),
+ borderRadiusGeometry: BorderRadius.vertical(
+ top: !details.isLastUser ? Radius.circular(16) : Radius.zero,
+ bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero,
+ ),
+ showSendingIndicator: DisplayWidget.gone,
+ reverse: false,
+ showUserAvatar:
+ details.isNextUser ? DisplayWidget.hide : DisplayWidget.show,
+ showTimestamp: !details.isNextUser,
+ showUsername: !details.isNextUser,
+ showReactions: false,
+ showReplyIndicator: false,
+ );
+ }
+}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 32af2bd3..f81c61be 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,10 +1,75 @@
+import 'dart:io';
+
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_apns/apns.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart'
+ hide Message;
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+void showLocalNotification(Message message, ChannelModel channel) async {
+ FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
+ FlutterLocalNotificationsPlugin();
+ final initializationSettingsAndroid =
+ AndroidInitializationSettings('launch_background');
+ final initializationSettingsIOS = IOSInitializationSettings();
+ final initializationSettings = InitializationSettings(
+ initializationSettingsAndroid,
+ initializationSettingsIOS,
+ );
+ await flutterLocalNotificationsPlugin.initialize(initializationSettings);
+ await flutterLocalNotificationsPlugin.show(
+ message.id.hashCode,
+ '${message.user.name} @ ${channel.name}',
+ message.text,
+ NotificationDetails(
+ AndroidNotificationDetails(
+ 'message channel',
+ 'Message channel',
+ 'Channel used for showing messages',
+ priority: Priority.High,
+ importance: Importance.High,
+ ),
+ IOSNotificationDetails(),
+ ),
+ );
+}
+
+Future backgroundHandler(Map notification) async {
+ final messageId = notification['data']['message_id'];
+
+ final notificationData =
+ await NotificationService.getAndStoreMessage(messageId);
+
+ showLocalNotification(
+ notificationData.message,
+ notificationData.channel,
+ );
+}
+
+void _initNotifications(Client client) {
+ final connector = createPushConnector();
+ connector.configure(
+ onBackgroundMessage: backgroundHandler,
+ );
+
+ connector.requestNotificationPermissions();
+ connector.token.addListener(() {
+ if (connector.token.value != null) {
+ client.addDevice(
+ connector.token.value,
+ Platform.isAndroid ? 'firebase' : 'apn',
+ );
+ }
+ });
+}
+
void main() async {
final client = Client(
's2dxdhpxd94g',
logLevel: Level.INFO,
+ showLocalNotification: Platform.isAndroid ? showLocalNotification : null,
+ persistenceEnabled: true,
);
await client.setUser(
@@ -12,6 +77,8 @@ void main() async {
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
+ _initNotifications(client);
+
runApp(MyApp(client));
}
@@ -26,12 +93,13 @@ class MyApp extends StatelessWidget {
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
- home: Container(
- child: StreamChat(
+ builder: (context, widget) {
+ return StreamChat(
+ child: widget,
client: client,
- child: ChannelListPage(),
- ),
- ),
+ );
+ },
+ home: ChannelListPage(),
);
}
}
@@ -40,17 +108,19 @@ class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: ChannelListView(
- filter: {
- 'members': {
- '\$in': [StreamChat.of(context).user.id],
- }
- },
- sort: [SortOption('last_message_at')],
- pagination: PaginationParams(
- limit: 20,
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ }
+ },
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
),
- channelWidget: ChannelPage(),
),
);
}
diff --git a/example/lib/multiple_conversation.dart b/example/lib/multiple_conversation.dart
index 56e5ed1e..647ae7dd 100644
--- a/example/lib/multiple_conversation.dart
+++ b/example/lib/multiple_conversation.dart
@@ -20,13 +20,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// [ChannelListView] handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel.
void main() async {
final client = Client(
- 'b67pax5b2wdq',
+ 's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
- User(id: 'falling-mountain-7'),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
runApp(MyApp(client));
@@ -40,12 +40,11 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
- home: Container(
- child: StreamChat(
- client: client,
- child: ChannelListPage(),
- ),
+ builder: (context, child) => StreamChat(
+ client: client,
+ child: child,
),
+ home: ChannelListPage(),
);
}
}
@@ -54,17 +53,19 @@ class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: ChannelListView(
- filter: {
- 'members': {
- '\$in': [StreamChat.of(context).user.id],
- }
- },
- sort: [SortOption('last_message_at')],
- pagination: PaginationParams(
- limit: 20,
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ }
+ },
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
),
- channelWidget: ChannelPage(),
),
);
}
diff --git a/example/lib/single_conversation.dart b/example/lib/single_conversation.dart
index a7863fc0..5f5d03ea 100644
--- a/example/lib/single_conversation.dart
+++ b/example/lib/single_conversation.dart
@@ -26,13 +26,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// If you now run the simulator you will see a single channel UI.
void main() async {
final client = Client(
- 'b67pax5b2wdq',
+ 's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
- User(id: 'falling-mountain-7'),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
final channel = client.channel('messaging', id: 'godevs');
diff --git a/example/lib/threads.dart b/example/lib/threads.dart
index 092a5c7f..f89e6fce 100644
--- a/example/lib/threads.dart
+++ b/example/lib/threads.dart
@@ -11,13 +11,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same [ThreadPage].
void main() async {
final client = Client(
- 'b67pax5b2wdq',
+ 's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
- User(id: 'falling-mountain-7'),
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
+ User(id: 'super-band-9'),
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
runApp(MyApp(client));
@@ -31,11 +31,12 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
+ builder: (context, child) => StreamChat(
+ child: child,
+ client: client,
+ ),
home: Container(
- child: StreamChat(
- client: client,
- child: ChannelListPage(),
- ),
+ child: ChannelListPage(),
),
);
}
@@ -45,17 +46,19 @@ class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: ChannelListView(
- filter: {
- 'members': {
- '\$in': [StreamChat.of(context).user.id],
- }
- },
- sort: [SortOption('last_message_at')],
- pagination: PaginationParams(
- limit: 20,
+ body: ChannelsBloc(
+ child: ChannelListView(
+ filter: {
+ 'members': {
+ '\$in': [StreamChat.of(context).user.id],
+ }
+ },
+ sort: [SortOption('last_message_at')],
+ pagination: PaginationParams(
+ limit: 20,
+ ),
+ channelWidget: ChannelPage(),
),
- channelWidget: ChannelPage(),
),
);
}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 2968dbca..266926c8 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -11,6 +11,8 @@ dependencies:
sdk: flutter
stream_chat_flutter:
path: ../
+ flutter_apns: ^1.1.0
+ flutter_local_notifications: ^1.4.3
dev_dependencies:
flutter_test:
diff --git a/lib/src/attachment_actions.dart b/lib/src/attachment_actions.dart
new file mode 100644
index 00000000..f101ef53
--- /dev/null
+++ b/lib/src/attachment_actions.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+class AttachmentActions extends StatelessWidget {
+ final Attachment attachment;
+ final Message message;
+
+ const AttachmentActions({
+ Key key,
+ this.attachment,
+ this.message,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final streamChannel = StreamChannel.of(context);
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisSize: MainAxisSize.min,
+ children: attachment.actions?.map((action) {
+ if (action.style == 'primary') {
+ return FlatButton(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Text('${action.text}'),
+ color: action.style == 'primary'
+ ? StreamChatTheme.of(context).accentColor
+ : null,
+ textColor: Colors.white,
+ onPressed: () {
+ streamChannel.channel.sendAction(message, {
+ action.name: action.value,
+ });
+ },
+ );
+ }
+ return OutlineButton(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Text('${action.text}'),
+ color: StreamChatTheme.of(context).accentColor,
+ onPressed: () {
+ streamChannel.channel.sendAction(message, {
+ action.name: action.value,
+ });
+ },
+ );
+ })?.toList(),
+ );
+ }
+}
diff --git a/lib/src/attachment_error.dart b/lib/src/attachment_error.dart
new file mode 100644
index 00000000..e08867b8
--- /dev/null
+++ b/lib/src/attachment_error.dart
@@ -0,0 +1,37 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+class AttachmentError extends StatelessWidget {
+ final Attachment attachment;
+ final Size size;
+
+ const AttachmentError({
+ Key key,
+ @required this.attachment,
+ this.size,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ if (attachment.localUri != null) {
+ return Image.file(
+ File(attachment.localUri.path),
+ );
+ }
+ return Center(
+ child: Container(
+ width: size?.width,
+ height: size?.height,
+ color: Color(0xffd0021B).withOpacity(.1),
+ child: Center(
+ child: Icon(
+ Icons.error_outline,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/attachment_title.dart b/lib/src/attachment_title.dart
new file mode 100644
index 00000000..ab9cd2da
--- /dev/null
+++ b/lib/src/attachment_title.dart
@@ -0,0 +1,56 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+import 'stream_chat_theme.dart';
+import 'utils.dart';
+
+class AttachmentTitle extends StatelessWidget {
+ const AttachmentTitle({
+ Key key,
+ @required this.attachment,
+ @required this.messageTheme,
+ }) : super(key: key);
+
+ final MessageTheme messageTheme;
+ final Attachment attachment;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () {
+ if (attachment.titleLink != null) {
+ launchURL(context, attachment.titleLink);
+ }
+ },
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ attachment.title,
+ overflow: TextOverflow.ellipsis,
+ style: messageTheme.messageText.copyWith(
+ color: StreamChatTheme.of(context).accentColor,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
+ Text(
+ Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
+ .authority
+ .split('.')
+ .reversed
+ .take(2)
+ .toList()
+ .reversed
+ .join('.'),
+ style: messageTheme.messageText,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart
index 5a7a146c..26668697 100644
--- a/lib/src/channel_header.dart
+++ b/lib/src/channel_header.dart
@@ -157,9 +157,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
child: Icon(
Icons.arrow_back_ios,
size: 15,
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white
- : Colors.black,
),
),
);
diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart
index a3918d9d..bef666db 100644
--- a/lib/src/channel_image.dart
+++ b/lib/src/channel_image.dart
@@ -43,6 +43,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelImage extends StatelessWidget {
+ /// Instantiate a new ChannelImage
const ChannelImage({
Key key,
this.channel,
diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart
index 1d5992c7..782dd772 100644
--- a/lib/src/channel_list_view.dart
+++ b/lib/src/channel_list_view.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/channels_bloc.dart';
import '../stream_chat_flutter.dart';
import 'channel_preview.dart';
@@ -46,6 +47,7 @@ typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelListView extends StatefulWidget {
+ /// Instantiate a new ChannelListView
ChannelListView({
Key key,
this.filter,
@@ -53,12 +55,14 @@ class ChannelListView extends StatefulWidget {
this.sort,
this.pagination,
this.onChannelTap,
+ this.onChannelLongPress,
this.channelWidget,
this.channelPreviewBuilder,
this.errorBuilder,
this.onImageTap,
}) : super(key: key);
+ /// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
/// The query filters to use.
@@ -89,6 +93,9 @@ class ChannelListView extends StatefulWidget {
/// with the widget [channelWidget] as child.
final ChannelTapCallback onChannelTap;
+ /// Function called when long pressing on a channel
+ final Function(Channel) onChannelLongPress;
+
/// Widget used when opening a channel
final Widget channelWidget;
@@ -102,16 +109,17 @@ class ChannelListView extends StatefulWidget {
_ChannelListViewState createState() => _ChannelListViewState();
}
-class _ChannelListViewState extends State {
+class _ChannelListViewState extends State
+ with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
- final streamChat = StreamChat.of(context);
+ final channelsProvider = ChannelsBloc.of(context);
+
return RefreshIndicator(
onRefresh: () async {
- streamChat.clearChannels();
- return streamChat.queryChannels(
+ return channelsProvider.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
@@ -119,7 +127,7 @@ class _ChannelListViewState extends State {
);
},
child: StreamBuilder>(
- stream: streamChat.channelsStream,
+ stream: channelsProvider.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
@@ -157,7 +165,7 @@ class _ChannelListViewState extends State {
TextSpan(text: 'Error loading channels'),
],
),
- style: Theme.of(context).textTheme.title,
+ style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
@@ -167,7 +175,7 @@ class _ChannelListViewState extends State {
),
FlatButton(
onPressed: () {
- streamChat.queryChannels(
+ channelsProvider.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
@@ -215,7 +223,7 @@ class _ChannelListViewState extends State {
i = i ~/ 2;
- final streamChat = StreamChat.of(context);
+ final channelsProvider = ChannelsBloc.of(context);
if (i < channels.length) {
final channel = channels[i];
@@ -267,6 +275,7 @@ class _ChannelListViewState extends State {
);
} else {
child = ChannelPreview(
+ onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: widget.onImageTap != null
? () {
@@ -283,15 +292,27 @@ class _ChannelListViewState extends State {
),
);
} else {
- return _buildQueryProgressIndicator(context, streamChat);
+ return _buildQueryProgressIndicator(context, channelsProvider);
}
}
- Widget _buildQueryProgressIndicator(context, StreamChatState streamChat) {
+ Widget _buildQueryProgressIndicator(
+ context, ChannelsBlocState channelsProvider) {
return StreamBuilder(
- stream: streamChat.queryChannelsLoading,
+ stream: channelsProvider.queryChannelsLoading,
initialData: false,
builder: (context, snapshot) {
+ if (snapshot.hasError) {
+ return Container(
+ color: Color(0xffd0021B).withAlpha(26),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0),
+ child: Center(
+ child: Text('Error loading channels'),
+ ),
+ ),
+ );
+ }
return Container(
height: 100,
padding: EdgeInsets.all(32),
@@ -312,14 +333,15 @@ class _ChannelListViewState extends State {
);
}
- void _listenChannelPagination(StreamChatState streamChat) {
+ void _listenChannelPagination(ChannelsBlocState channelsProvider) {
if (_scrollController.position.maxScrollExtent ==
- _scrollController.offset) {
- streamChat.queryChannels(
+ _scrollController.offset &&
+ _scrollController.offset != 0) {
+ channelsProvider.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination.copyWith(
- offset: streamChat.channels.length,
+ offset: channelsProvider.channels.length,
),
options: widget.options,
);
@@ -330,8 +352,11 @@ class _ChannelListViewState extends State {
void initState() {
super.initState();
- final streamChat = StreamChat.of(context);
- streamChat.queryChannels(
+ final channelsBloc = ChannelsBloc.of(context);
+
+ WidgetsBinding.instance.addObserver(this);
+
+ channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
@@ -339,7 +364,34 @@ class _ChannelListViewState extends State {
);
_scrollController.addListener(() {
- _listenChannelPagination(streamChat);
+ channelsBloc.queryChannelsLoading.first.then((loading) {
+ if (!loading) {
+ _listenChannelPagination(channelsBloc);
+ }
+ });
+ });
+
+ final client = StreamChat.of(context).client;
+
+ client
+ .on(
+ EventType.connectionRecovered,
+ EventType.notificationAddedToChannel,
+ EventType.channelVisible,
+ )
+ .listen((event) {
+ channelsBloc.queryChannels(
+ filter: widget.filter,
+ sortOptions: widget.sort,
+ paginationParams: widget.pagination,
+ options: widget.options,
+ );
});
}
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
}
diff --git a/lib/src/channel_name.dart b/lib/src/channel_name.dart
index 2200b351..ef7599b7 100644
--- a/lib/src/channel_name.dart
+++ b/lib/src/channel_name.dart
@@ -8,6 +8,7 @@ import 'stream_channel.dart';
///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
class ChannelName extends StatelessWidget {
+ /// Instantiate a new ChannelName
const ChannelName({
Key key,
this.channel,
diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart
index 9b949a68..5a3432aa 100644
--- a/lib/src/channel_preview.dart
+++ b/lib/src/channel_preview.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/unread_indicator.dart';
import '../stream_chat_flutter.dart';
import 'channel_name.dart';
@@ -22,6 +23,9 @@ class ChannelPreview extends StatelessWidget {
/// Function called when tapping this widget
final void Function(Channel) onTap;
+ /// Function called when long pressing this widget
+ final void Function(Channel) onLongPress;
+
/// Channel displayed
final Channel channel;
@@ -32,6 +36,7 @@ class ChannelPreview extends StatelessWidget {
@required this.channel,
Key key,
this.onTap,
+ this.onLongPress,
this.onImageTap,
}) : super(key: key);
@@ -39,7 +44,14 @@ class ChannelPreview extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
onTap: () {
- onTap(channel);
+ if (onTap != null) {
+ onTap(channel);
+ }
+ },
+ onLongPress: () {
+ if (onLongPress != null) {
+ onLongPress(channel);
+ }
},
leading: ChannelImage(
onTap: onImageTap,
@@ -53,16 +65,8 @@ class ChannelPreview extends StatelessWidget {
children: [
_buildDate(context),
if (channel.state.unreadCount > 0)
- Padding(
- padding: const EdgeInsets.only(left: 8.0),
- child: CircleAvatar(
- backgroundColor: Color(0xffd0021B),
- radius: 6,
- child: Text(
- '${channel.state.unreadCount}',
- style: TextStyle(fontSize: 8),
- ),
- ),
+ UnreadIndicator(
+ channel: channel,
),
],
),
@@ -118,16 +122,15 @@ class ChannelPreview extends StatelessWidget {
stream: channel.state.messagesStream,
initialData: channel.state.messages,
builder: (context, snapshot) {
- final messages = snapshot.data;
- final lastMessage = messages.isNotEmpty ? messages.last : null;
+ final lastMessage = channel.state.lastMessage;
if (lastMessage == null) {
return SizedBox();
}
- String text;
+ var text = lastMessage.text;
if (lastMessage.isDeleted) {
text = 'This message was deleted.';
- } else {
+ } else if (lastMessage.attachments != null) {
final prefix = lastMessage.attachments
.map((e) {
if (e.type == 'image') {
diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart
new file mode 100644
index 00000000..b38c6030
--- /dev/null
+++ b/lib/src/channels_bloc.dart
@@ -0,0 +1,144 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:rxdart/rxdart.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/stream_chat.dart';
+
+/// Widget dedicated to the management of a channel list with pagination
+class ChannelsBloc extends StatefulWidget {
+ /// The widget child
+ final Widget child;
+
+ /// Instantiate a new ChannelsBloc
+ const ChannelsBloc({
+ Key key,
+ this.child,
+ }) : super(key: key);
+
+ @override
+ ChannelsBlocState createState() => ChannelsBlocState();
+
+ /// Use this method to get the current [ChannelsBlocState] instance
+ static ChannelsBlocState of(BuildContext context) {
+ ChannelsBlocState streamChatState;
+
+ streamChatState = context.findAncestorStateOfType();
+
+ if (streamChatState == null) {
+ throw Exception('You must have a ChannelsBloc widget as anchestor');
+ }
+
+ return streamChatState;
+ }
+}
+
+/// The current state of the [ChannelsBloc]
+class ChannelsBlocState extends State
+ with AutomaticKeepAliveClientMixin {
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return widget.child;
+ }
+
+ /// The current channel list
+ List get channels => _channelsController.value;
+
+ /// The current channel list as a stream
+ Stream> get channelsStream => _channelsController.stream;
+
+ final BehaviorSubject _queryChannelsLoadingController =
+ BehaviorSubject.seeded(false);
+
+ final BehaviorSubject> _channelsController =
+ BehaviorSubject.seeded([]);
+
+ /// The stream notifying the state of queryChannel call
+ Stream get queryChannelsLoading =>
+ _queryChannelsLoadingController.stream;
+
+ /// Calls [client.queryChannels] updating [queryChannelsLoading] stream
+ Future queryChannels({
+ Map filter,
+ List sortOptions,
+ PaginationParams paginationParams,
+ Map options,
+ bool onlyOffline = false,
+ }) async {
+ if (_queryChannelsLoadingController.value == true) {
+ return;
+ }
+ _queryChannelsLoadingController.sink.add(true);
+
+ try {
+ final clear = paginationParams == null ||
+ paginationParams.offset == null ||
+ paginationParams.offset == 0;
+ final oldChannels = List.from(channels);
+ StreamChat.of(context)
+ .client
+ .queryChannels(
+ filter: filter,
+ sort: sortOptions,
+ options: options,
+ paginationParams: paginationParams,
+ onlyOffline: onlyOffline,
+ )
+ .listen((channels) {
+ if (clear) {
+ _channelsController.add(channels);
+ } else {
+ final l = oldChannels + channels;
+ _channelsController.add(l);
+ }
+ }, onDone: () {
+ _queryChannelsLoadingController.sink.add(false);
+ }, onError: (err, stackTrace) {
+ print(err);
+ print(stackTrace);
+ _queryChannelsLoadingController.addError(err, stackTrace);
+ });
+ } catch (err, stackTrace) {
+ _queryChannelsLoadingController.addError(err, stackTrace);
+ }
+ }
+
+ final List _subscriptions = [];
+
+ @override
+ void initState() {
+ super.initState();
+
+ final client = StreamChat.of(context).client;
+
+ _subscriptions.add(client.on(EventType.messageNew).listen((e) {
+ final newChannels = List.from(channels ?? []);
+ final index = newChannels.indexWhere((c) => c.cid == e.cid);
+ if (index > 0) {
+ final channel = newChannels.removeAt(index);
+ newChannels.insert(0, channel);
+ _channelsController.add(newChannels);
+ }
+ }));
+
+ _subscriptions.add(client
+ .on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
+ .listen((e) {
+ final channel = e.channel;
+ _channelsController
+ .add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
+ }));
+ }
+
+ @override
+ void dispose() {
+ _channelsController.close();
+ _queryChannelsLoadingController.close();
+ _subscriptions.forEach((s) => s.cancel());
+ super.dispose();
+ }
+
+ @override
+ bool get wantKeepAlive => true;
+}
diff --git a/lib/src/date_divider.dart b/lib/src/date_divider.dart
new file mode 100644
index 00000000..102c6e14
--- /dev/null
+++ b/lib/src/date_divider.dart
@@ -0,0 +1,79 @@
+import 'package:flutter/material.dart';
+import 'package:jiffy/jiffy.dart';
+
+/// It shows a date divider depending on the date difference
+class DateDivider extends StatelessWidget {
+ final DateTime dateTime;
+
+ const DateDivider({
+ Key key,
+ @required this.dateTime,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final divider = Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Divider(),
+ ),
+ );
+
+ final createdAt = Jiffy(dateTime);
+ final now = DateTime.now();
+ final hourInfo = createdAt.format('h:mm a');
+
+ String dayInfo;
+ if (Jiffy(createdAt).isSame(now, Units.DAY)) {
+ dayInfo = 'TODAY';
+ } else if (Jiffy(createdAt)
+ .isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
+ dayInfo = 'YESTERDAY';
+ } else if (Jiffy(createdAt).isAfter(
+ now.subtract(Duration(days: 7)),
+ Units.DAY,
+ )) {
+ dayInfo = createdAt.format('EEEE').toUpperCase();
+ } else if (Jiffy(createdAt).isAfter(
+ Jiffy(now).subtract(years: 1),
+ Units.DAY,
+ )) {
+ dayInfo = createdAt.format('dd/MM').toUpperCase();
+ } else {
+ dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
+ }
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ divider,
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32.0),
+ child: Text.rich(
+ TextSpan(
+ children: [
+ TextSpan(
+ text: dayInfo,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ TextSpan(text: ' AT'),
+ TextSpan(text: ' $hourInfo'),
+ ],
+ style: TextStyle(
+ fontWeight: FontWeight.normal,
+ ),
+ ),
+ style: TextStyle(
+ fontSize: 10,
+ color:
+ Theme.of(context).textTheme.headline6.color.withOpacity(.5),
+ ),
+ ),
+ ),
+ divider,
+ ],
+ );
+ }
+}
diff --git a/lib/src/deleted_message.dart b/lib/src/deleted_message.dart
new file mode 100644
index 00000000..93fd62ff
--- /dev/null
+++ b/lib/src/deleted_message.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
+
+class DeletedMessage extends StatelessWidget {
+ const DeletedMessage({
+ Key key,
+ @required this.messageTheme,
+ }) : super(key: key);
+
+ final MessageTheme messageTheme;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Text(
+ 'This message was deleted...',
+ style: messageTheme.messageText.copyWith(
+ fontStyle: FontStyle.italic,
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white
+ : Colors.black,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/file_attachment.dart b/lib/src/file_attachment.dart
new file mode 100644
index 00000000..05318862
--- /dev/null
+++ b/lib/src/file_attachment.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/utils.dart';
+
+class FileAttachment extends StatelessWidget {
+ final Attachment attachment;
+ final Size size;
+
+ const FileAttachment({
+ Key key,
+ @required this.attachment,
+ this.size,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ child: InkWell(
+ onTap: () {
+ launchURL(context, attachment.assetUrl);
+ },
+ child: Container(
+ width: size?.width ?? 100,
+ height: size?.height ?? 100,
+ child: Center(
+ child: Icon(Icons.attach_file),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/full_screen_image.dart b/lib/src/full_screen_image.dart
new file mode 100644
index 00000000..c6f5804f
--- /dev/null
+++ b/lib/src/full_screen_image.dart
@@ -0,0 +1,29 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:photo_view/photo_view.dart';
+
+/// A full screen image widget
+class FullScreenImage extends StatelessWidget {
+ /// The url of the image
+ final String url;
+
+ /// Instantiate a new FullScreenImage
+ const FullScreenImage({
+ Key key,
+ @required this.url,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ child: PhotoView(
+ imageProvider: CachedNetworkImageProvider(url),
+ maxScale: PhotoViewComputedScale.covered,
+ minScale: PhotoViewComputedScale.contained,
+ heroAttributes: PhotoViewHeroAttributes(
+ tag: url,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/full_screen_video.dart b/lib/src/full_screen_video.dart
new file mode 100644
index 00000000..46325b07
--- /dev/null
+++ b/lib/src/full_screen_video.dart
@@ -0,0 +1,78 @@
+import 'package:chewie/chewie.dart';
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+import 'package:video_player/video_player.dart';
+
+import 'utils.dart';
+
+class FullScreenVideo extends StatefulWidget {
+ final Attachment attachment;
+
+ FullScreenVideo({
+ Key key,
+ @required this.attachment,
+ }) : super(key: key);
+
+ @override
+ _FullScreenVideoState createState() => _FullScreenVideoState();
+}
+
+class _FullScreenVideoState extends State {
+ ChewieController _chewieController;
+ VideoPlayerController _videoPlayerController;
+ bool initialized = false;
+ final GlobalKey _scaffoldKey = GlobalKey();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Builder(
+ key: _scaffoldKey,
+ builder: (context) {
+ if (!initialized) {
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+ return Chewie(
+ controller: _chewieController,
+ );
+ },
+ ),
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _videoPlayerController =
+ VideoPlayerController.network(widget.attachment.assetUrl);
+ _videoPlayerController.initialize().whenComplete(() {
+ setState(() {
+ initialized = true;
+ _chewieController = ChewieController(
+ videoPlayerController: _videoPlayerController,
+ autoInitialize: false,
+ aspectRatio: _videoPlayerController.value.aspectRatio,
+ );
+ });
+ });
+
+ VoidCallback errorListener;
+ errorListener = () {
+ if (_videoPlayerController.value.hasError) {
+ Navigator.pop(context);
+ launchURL(_scaffoldKey.currentContext, widget.attachment.titleLink);
+ }
+ _videoPlayerController.removeListener(errorListener);
+ };
+ _videoPlayerController.addListener(errorListener);
+ }
+
+ @override
+ void dispose() {
+ _videoPlayerController?.dispose();
+ _chewieController?.dispose();
+ super.dispose();
+ }
+}
diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart
new file mode 100644
index 00000000..62de980b
--- /dev/null
+++ b/lib/src/giphy_attachment.dart
@@ -0,0 +1,93 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/src/attachment_actions.dart';
+
+import '../stream_chat_flutter.dart';
+import 'attachment_error.dart';
+import 'attachment_title.dart';
+import 'full_screen_image.dart';
+
+class GiphyAttachment extends StatelessWidget {
+ final Attachment attachment;
+ final MessageTheme messageTheme;
+ final Message message;
+ final Size size;
+
+ const GiphyAttachment({
+ Key key,
+ this.attachment,
+ this.messageTheme,
+ this.message,
+ this.size,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ if (attachment.thumbUrl == null &&
+ attachment.imageUrl == null &&
+ attachment.assetUrl == null) {
+ return AttachmentError(
+ attachment: attachment,
+ );
+ }
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Stack(
+ children: [
+ GestureDetector(
+ onTap: () {
+ Navigator.push(context, MaterialPageRoute(builder: (_) {
+ return FullScreenImage(
+ url: attachment.imageUrl ??
+ attachment.assetUrl ??
+ attachment.thumbUrl,
+ );
+ }));
+ },
+ child: CachedNetworkImage(
+ height: size?.height,
+ width: size?.width,
+ placeholder: (_, __) {
+ return Container(
+ width: size?.width,
+ height: size?.height,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ },
+ imageUrl: attachment.thumbUrl ??
+ attachment.imageUrl ??
+ attachment.assetUrl,
+ errorWidget: (context, url, error) => AttachmentError(
+ attachment: attachment,
+ size: size,
+ ),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ],
+ ),
+ if (attachment.title != null)
+ Container(
+ alignment: Alignment.bottomCenter,
+ child: Material(
+ color: messageTheme.messageBackgroundColor,
+ child: AttachmentTitle(
+ messageTheme: messageTheme,
+ attachment: attachment,
+ ),
+ ),
+ ),
+ if (attachment.actions != null)
+ AttachmentActions(
+ attachment: attachment,
+ message: message,
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart
new file mode 100644
index 00000000..a4bd1a0f
--- /dev/null
+++ b/lib/src/image_attachment.dart
@@ -0,0 +1,99 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+
+import '../stream_chat_flutter.dart';
+import 'attachment_error.dart';
+import 'attachment_title.dart';
+import 'full_screen_image.dart';
+import 'utils.dart';
+
+class ImageAttachment extends StatelessWidget {
+ final Attachment attachment;
+ final Message message;
+ final MessageTheme messageTheme;
+ final Size size;
+
+ const ImageAttachment({
+ Key key,
+ @required this.attachment,
+ @required this.message,
+ this.messageTheme,
+ this.size,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ if (attachment.thumbUrl == null &&
+ attachment.imageUrl == null &&
+ attachment.assetUrl == null) {
+ return AttachmentError(
+ attachment: attachment,
+ );
+ }
+ return SizedBox.fromSize(
+ size: size,
+ child: Stack(
+ children: [
+ Column(
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: () {
+ Navigator.push(context, MaterialPageRoute(builder: (_) {
+ return FullScreenImage(
+ url: attachment.imageUrl ??
+ attachment.assetUrl ??
+ attachment.thumbUrl,
+ );
+ }));
+ },
+ child: CachedNetworkImage(
+ height: size?.height,
+ width: size?.width,
+ placeholder: (_, __) {
+ return Container(
+ width: size?.width,
+ height: size?.height,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ },
+ imageUrl: attachment.thumbUrl ??
+ attachment.imageUrl ??
+ attachment.assetUrl,
+ errorWidget: (context, url, error) => AttachmentError(
+ attachment: attachment,
+ size: size,
+ ),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ if (attachment.title != null)
+ Material(
+ color: messageTheme.messageBackgroundColor,
+ child: AttachmentTitle(
+ messageTheme: messageTheme,
+ attachment: attachment,
+ ),
+ ),
+ ],
+ ),
+ if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
+ Positioned.fill(
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => launchURL(
+ context,
+ attachment.titleLink ?? attachment.ogScrapeUrl,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/message_actions_bottom_sheet.dart b/lib/src/message_actions_bottom_sheet.dart
new file mode 100644
index 00000000..1e81900f
--- /dev/null
+++ b/lib/src/message_actions_bottom_sheet.dart
@@ -0,0 +1,209 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/reaction_picker.dart';
+import 'package:stream_chat_flutter/src/stream_channel.dart';
+
+import '../stream_chat_flutter.dart';
+import 'message_input.dart';
+import 'stream_chat.dart';
+
+class MessageActionsBottomSheet extends StatelessWidget {
+ final Widget Function(BuildContext, Message) editMessageInputBuilder;
+ final void Function(Message) onThreadTap;
+ final Message message;
+ final bool showReactions;
+ final bool showDeleteMessage;
+ final bool showEditMessage;
+ final bool showReply;
+ final Map reactionToEmoji = const {
+ 'love': '❤️️',
+ 'haha': '😂',
+ 'like': '👍',
+ 'sad': '😕',
+ 'angry': '😡',
+ 'wow': '😲',
+ };
+
+ const MessageActionsBottomSheet({
+ Key key,
+ this.message,
+ this.showReactions,
+ this.showDeleteMessage,
+ this.showEditMessage,
+ this.onThreadTap,
+ this.showReply,
+ this.editMessageInputBuilder,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final channel = StreamChannel.of(context).channel;
+ return SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (showReactions &&
+ (message.status == MessageSendingStatus.SENT ||
+ message.status == null))
+ ReactionPicker(
+ channel: channel,
+ reactionToEmoji: reactionToEmoji,
+ message: message,
+ ),
+ if (showDeleteMessage) _buildDeleteButton(context),
+ if (showEditMessage) _buildEditMessage(context),
+ if (showReply &&
+ (message.status == MessageSendingStatus.SENT ||
+ message.status == null) &&
+ message.parentId == null)
+ _buildReplyButton(context),
+ ],
+ ),
+ );
+ }
+
+ FlatButton _buildDeleteButton(BuildContext context) {
+ return FlatButton(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ 'Delete message',
+ style:
+ Theme.of(context).textTheme.headline5.copyWith(color: Colors.red),
+ ),
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ StreamChat.of(context).client.deleteMessage(
+ message,
+ StreamChannel.of(context).channel.cid,
+ );
+ },
+ );
+ }
+
+ FlatButton _buildEditMessage(BuildContext context) {
+ return FlatButton(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ 'Edit message',
+ style: Theme.of(context).textTheme.headline5,
+ ),
+ ),
+ onPressed: () async {
+ Navigator.pop(context);
+ _showEditBottomSheet(context);
+ },
+ );
+ }
+
+ void _showEditBottomSheet(BuildContext context) {
+ final channel = StreamChannel.of(context).channel;
+ showModalBottomSheet(
+ context: context,
+ elevation: 2,
+ clipBehavior: Clip.hardEdge,
+ isScrollControlled: true,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(32),
+ topRight: Radius.circular(32),
+ ),
+ ),
+ builder: (context) {
+ return StreamChannel(
+ channel: channel,
+ child: Flex(
+ direction: Axis.vertical,
+ mainAxisAlignment: MainAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 16.0,
+ left: 16.0,
+ right: 16.0,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Edit message',
+ style: Theme.of(context).textTheme.headline6,
+ ),
+ Container(
+ height: 30,
+ padding: const EdgeInsets.all(2.0),
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: RawMaterialButton(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(4),
+ ),
+ elevation: 0,
+ highlightElevation: 0,
+ focusElevation: 0,
+ disabledElevation: 0,
+ hoverElevation: 0,
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ fillColor:
+ Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(.1)
+ : Colors.black.withOpacity(.1),
+ padding: EdgeInsets.all(4),
+ child: Icon(
+ Icons.close,
+ size: 15,
+ color: StreamChatTheme.of(context)
+ .primaryIconTheme
+ .color,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).viewInsets.bottom,
+ ),
+ child: editMessageInputBuilder != null
+ ? editMessageInputBuilder(context, message)
+ : MessageInput(
+ editMessage: message,
+ onMessageSent: (_) {
+ FocusScope.of(context).unfocus();
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ FlatButton _buildReplyButton(BuildContext context) {
+ return FlatButton(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ 'Start a thread',
+ style: Theme.of(context).textTheme.headline5,
+ ),
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ if (onThreadTap != null) {
+ onThreadTap(message);
+ }
+ },
+ );
+ }
+}
diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart
index 6338732b..6358e39e 100644
--- a/lib/src/message_input.dart
+++ b/lib/src/message_input.dart
@@ -17,6 +17,21 @@ import '../stream_chat_flutter.dart';
import 'stream_channel.dart';
typedef FileUploader = Future Function(File, Channel);
+typedef AttachmentThumbnailBuilder = Widget Function(
+ BuildContext,
+ _SendingAttachment,
+);
+
+enum ActionsLocation {
+ left,
+ right,
+}
+
+enum DefaultAttachmentTypes {
+ image,
+ video,
+ file,
+}
/// Inactive state
/// 
@@ -61,9 +76,11 @@ typedef FileUploader = Future Function(File, Channel);
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageInput extends StatefulWidget {
+ /// Instantiate a new MessageInput
MessageInput({
Key key,
this.onMessageSent,
+ this.preMessageSending,
this.parentMessage,
this.editMessage,
this.maxHeight = 150,
@@ -72,6 +89,10 @@ class MessageInput extends StatefulWidget {
this.doImageUploadRequest,
this.doFileUploadRequest,
this.initialMessage,
+ this.textEditingController,
+ this.actions,
+ this.actionsLocation = ActionsLocation.left,
+ this.attachmentThumbnailBuilders,
}) : super(key: key);
/// Message to edit
@@ -83,6 +104,10 @@ class MessageInput extends StatefulWidget {
/// Function called after sending the message
final void Function(Message) onMessageSent;
+ /// Function called right before sending the message
+ /// Use this to transform the message
+ final FutureOr Function(Message) preMessageSending;
+
/// Parent message in case of a thread
final Message parentMessage;
@@ -101,33 +126,49 @@ class MessageInput extends StatefulWidget {
/// Override file upload request
final FileUploader doFileUploadRequest;
+ /// The text controller of the TextField
+ final TextEditingController textEditingController;
+
+ /// List of action widgets
+ final List actions;
+
+ /// The location of the custom actions
+ final ActionsLocation actionsLocation;
+
+ /// Map that defines a thumbnail builder for an attachment type
+ final Map attachmentThumbnailBuilders;
+
@override
- _MessageInputState createState() => _MessageInputState(
- doFileUploadRequest: doFileUploadRequest,
- doImageUploadRequest: doImageUploadRequest,
- );
+ MessageInputState createState() => MessageInputState();
+
+ /// Use this method to get the current [StreamChatState] instance
+ static MessageInputState of(BuildContext context) {
+ MessageInputState messageInputState;
+
+ messageInputState = context.findAncestorStateOfType();
+
+ if (messageInputState == null) {
+ throw Exception(
+ 'You must have a MessageInput widget as anchestor of your widget tree');
+ }
+
+ return messageInputState;
+ }
}
-class _MessageInputState extends State {
+class MessageInputState extends State {
final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode();
final List _mentionedUsers = [];
- FileUploader doImageUploadRequest;
- FileUploader doFileUploadRequest;
- TextEditingController _textController;
+ final _imagePicker = ImagePicker();
bool _inputEnabled = true;
bool _messageIsPresent = false;
bool _typingStarted = false;
OverlayEntry _commandsOverlay, _mentionsOverlay;
- _MessageInputState({
- this.doImageUploadRequest,
- this.doFileUploadRequest,
- }) {
- doImageUploadRequest ??= _uploadImage;
- doFileUploadRequest ??= _uploadFile;
- }
+ /// The editing controller passed to the input TextField
+ TextEditingController textEditingController;
@override
Widget build(BuildContext context) {
@@ -164,8 +205,12 @@ class _MessageInputState extends State {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!widget.disableAttachments) _buildAttachmentButton(),
+ if (widget.actionsLocation == ActionsLocation.left)
+ ...widget.actions ?? [],
_buildTextInput(context),
_animateSendButton(context),
+ if (widget.actionsLocation == ActionsLocation.right)
+ ...widget.actions ?? [],
],
);
}
@@ -193,10 +238,10 @@ class _MessageInputState extends State {
minLines: null,
maxLines: null,
onSubmitted: (_) {
- _sendMessage(context);
+ sendMessage();
},
keyboardType: widget.keyboardType,
- controller: _textController,
+ controller: textEditingController,
focusNode: _focusNode,
onChanged: (s) {
StreamChannel.of(context).channel.keyStroke();
@@ -215,10 +260,10 @@ class _MessageInputState extends State {
Overlay.of(context).insert(_commandsOverlay);
}
- if (_textController.selection.isCollapsed &&
- (s[_textController.selection.start - 1] == '@' ||
- _textController.text
- .substring(0, _textController.selection.start)
+ if (textEditingController.selection.isCollapsed &&
+ (s[textEditingController.selection.start - 1] == '@' ||
+ textEditingController.text
+ .substring(0, textEditingController.selection.start)
.split(' ')
.last
.contains('@'))) {
@@ -231,7 +276,7 @@ class _MessageInputState extends State {
_typingStarted = true;
});
},
- style: Theme.of(context).textTheme.body1,
+ style: Theme.of(context).textTheme.bodyText2,
autofocus: false,
decoration: InputDecoration(
hintText: 'Write a message',
@@ -278,7 +323,7 @@ class _MessageInputState extends State {
}
OverlayEntry _buildCommandsOverlayEntry() {
- final text = _textController.text;
+ final text = textEditingController.text;
final commands = StreamChannel.of(context)
.channel
.config
@@ -343,11 +388,21 @@ class _MessageInputState extends State {
}
OverlayEntry _buildMentionsOverlayEntry() {
- final splits = _textController.text
- .substring(0, _textController.value.selection.start)
+ final splits = textEditingController.text
+ .substring(0, textEditingController.value.selection.start)
.split('@');
final query = splits.last.toLowerCase();
+ Future> queryMembers;
+
+ if (query.isNotEmpty) {
+ queryMembers = StreamChannel.of(context).channel.queryMembers(filter: {
+ 'name': {
+ '\$autocomplete': query,
+ },
+ }).then((res) => res.members);
+ }
+
final members = StreamChannel.of(context).channel.state.members?.where((m) {
return m.user.name.toLowerCase().contains(query);
})?.toList() ??
@@ -375,36 +430,42 @@ class _MessageInputState extends State {
],
color: StreamChatTheme.of(context).primaryColor,
),
- child: ListView(
- padding: const EdgeInsets.all(0),
- shrinkWrap: true,
- children: members
- .map((m) => ListTile(
- leading: UserAvatar(
- user: m.user,
- ),
- title: Text('${m.user.name}'),
- onTap: () {
- _mentionedUsers.add(m.user);
+ child: FutureBuilder>(
+ future: queryMembers ?? Future.value(members),
+ initialData: members,
+ builder: (context, snapshot) {
+ return ListView(
+ padding: const EdgeInsets.all(0),
+ shrinkWrap: true,
+ children: snapshot.data
+ .map((m) => ListTile(
+ leading: UserAvatar(
+ user: m.user,
+ ),
+ title: Text('${m.user.name}'),
+ onTap: () {
+ _mentionedUsers.add(m.user);
- splits[splits.length - 1] = m.user.name;
- final rejoin = splits.join('@');
+ splits[splits.length - 1] = m.user.name;
+ final rejoin = splits.join('@');
- _textController.value = TextEditingValue(
- text: rejoin +
- _textController.text
- .substring(_textController.selection.start),
- selection: TextSelection.collapsed(
- offset: rejoin.length,
- ),
- );
+ textEditingController.value = TextEditingValue(
+ text: rejoin +
+ textEditingController.text.substring(
+ textEditingController
+ .selection.start),
+ selection: TextSelection.collapsed(
+ offset: rejoin.length,
+ ),
+ );
- _mentionsOverlay?.remove();
- _mentionsOverlay = null;
- },
- ))
- .toList(),
- ),
+ _mentionsOverlay?.remove();
+ _mentionsOverlay = null;
+ },
+ ))
+ .toList(),
+ );
+ }),
),
),
);
@@ -412,7 +473,7 @@ class _MessageInputState extends State {
}
void _setCommand(Command c) {
- _textController.value = TextEditingValue(
+ textEditingController.value = TextEditingValue(
text: '/${c.name} ',
selection: TextSelection.collapsed(
offset: c.name.length + 2,
@@ -451,35 +512,7 @@ class _MessageInputState extends State {
width: 50,
child: _buildAttachment(attachment),
),
- Positioned(
- height: 16,
- width: 16,
- top: 4,
- right: 4,
- child: RawMaterialButton(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- ),
- elevation: 0,
- highlightElevation: 0,
- focusElevation: 0,
- disabledElevation: 0,
- hoverElevation: 0,
- onPressed: () {
- setState(() {
- _attachments.remove(attachment);
- });
- },
- fillColor: Colors.white.withOpacity(.5),
- child: Center(
- child: Icon(
- Icons.close,
- size: 15,
- color: Colors.black,
- ),
- ),
- ),
- ),
+ _buildRemoveButton(attachment),
attachment.uploaded
? SizedBox()
: Positioned.fill(
@@ -499,20 +532,62 @@ class _MessageInputState extends State {
);
}
+ Positioned _buildRemoveButton(_SendingAttachment attachment) {
+ return Positioned(
+ height: 16,
+ width: 16,
+ top: 4,
+ right: 4,
+ child: RawMaterialButton(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ elevation: 0,
+ highlightElevation: 0,
+ focusElevation: 0,
+ disabledElevation: 0,
+ hoverElevation: 0,
+ onPressed: () {
+ setState(() {
+ _attachments.remove(attachment);
+ });
+ },
+ fillColor: Colors.white.withOpacity(.5),
+ child: Center(
+ child: Icon(
+ Icons.close,
+ size: 15,
+ ),
+ ),
+ ),
+ );
+ }
+
Widget _buildAttachment(_SendingAttachment attachment) {
- switch (attachment.type) {
- case FileType.image:
+ if (widget.attachmentThumbnailBuilders
+ ?.containsKey(attachment.attachment.type) ==
+ true) {
+ return widget.attachmentThumbnailBuilders[attachment.attachment.type](
+ context,
+ attachment,
+ );
+ }
+
+ switch (attachment.attachment.type) {
+ case 'image':
+ case 'giphy':
return attachment.file != null
? Image.file(
attachment.file,
fit: BoxFit.cover,
)
: Image.network(
- attachment.url,
+ attachment.attachment.imageUrl ??
+ attachment.attachment.thumbUrl,
fit: BoxFit.cover,
);
break;
- case FileType.video:
+ case 'video':
return Container(
child: Icon(Icons.videocam),
color: Colors.black26,
@@ -535,7 +610,7 @@ class _MessageInputState extends State {
color: Colors.transparent,
child: IconButton(
onPressed: () {
- _showAttachmentModal();
+ showAttachmentModal();
},
icon: Icon(
Icons.add_circle_outline,
@@ -544,7 +619,8 @@ class _MessageInputState extends State {
);
}
- void _showAttachmentModal() {
+ /// Show the attachment modal, making the user choose where to pick a media from
+ void showAttachmentModal() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
@@ -575,7 +651,7 @@ class _MessageInputState extends State {
leading: Icon(Icons.image),
title: Text('Upload a photo'),
onTap: () {
- _pickFile(FileType.image, false);
+ pickFile(DefaultAttachmentTypes.image, false);
Navigator.pop(context);
},
),
@@ -583,7 +659,7 @@ class _MessageInputState extends State {
leading: Icon(Icons.video_library),
title: Text('Upload a video'),
onTap: () {
- _pickFile(FileType.video, false);
+ pickFile(DefaultAttachmentTypes.video, false);
Navigator.pop(context);
},
),
@@ -591,7 +667,7 @@ class _MessageInputState extends State {
leading: Icon(Icons.camera_alt),
title: Text('Photo from camera'),
onTap: () {
- _pickFile(FileType.image, true);
+ pickFile(DefaultAttachmentTypes.image, true);
Navigator.pop(context);
},
),
@@ -599,7 +675,7 @@ class _MessageInputState extends State {
leading: Icon(Icons.videocam),
title: Text('Video from camera'),
onTap: () {
- _pickFile(FileType.video, true);
+ pickFile(DefaultAttachmentTypes.video, true);
Navigator.pop(context);
},
),
@@ -607,7 +683,7 @@ class _MessageInputState extends State {
leading: Icon(Icons.insert_drive_file),
title: Text('Upload a file'),
onTap: () {
- _pickFile(FileType.any, false);
+ pickFile(DefaultAttachmentTypes.file, false);
Navigator.pop(context);
},
),
@@ -616,20 +692,52 @@ class _MessageInputState extends State {
});
}
- void _pickFile(FileType type, bool camera) async {
+ /// Add an attachment to the sending message
+ /// Use this to add custom type attachments
+ void addAttachment(Attachment attachment) {
+ setState(() {
+ _attachments.add(_SendingAttachment(
+ attachment: attachment,
+ uploaded: true,
+ ));
+ });
+ }
+
+ /// Pick a file from the device
+ /// If [camera] is true then the camera will open
+ void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
setState(() {
_inputEnabled = false;
});
File file;
+ String attachmentType;
+
+ if (fileType == DefaultAttachmentTypes.image) {
+ attachmentType = 'image';
+ } else if (fileType == DefaultAttachmentTypes.video) {
+ attachmentType = 'video';
+ } else if (fileType == DefaultAttachmentTypes.file) {
+ attachmentType = 'file';
+ }
if (camera) {
- if (type == FileType.image) {
- file = await ImagePicker.pickImage(source: ImageSource.camera);
- } else if (type == FileType.video) {
- file = await ImagePicker.pickVideo(source: ImageSource.camera);
+ PickedFile pickedFile;
+ if (fileType == DefaultAttachmentTypes.image) {
+ pickedFile = await _imagePicker.getImage(source: ImageSource.camera);
+ } else if (fileType == DefaultAttachmentTypes.video) {
+ pickedFile = await _imagePicker.getVideo(source: ImageSource.camera);
}
+ file = File(pickedFile.path);
} else {
+ FileType type;
+ if (fileType == DefaultAttachmentTypes.image) {
+ type = FileType.image;
+ } else if (fileType == DefaultAttachmentTypes.video) {
+ type = FileType.video;
+ } else if (fileType == DefaultAttachmentTypes.file) {
+ type = FileType.any;
+ }
file = await FilePicker.getFile(type: type);
}
@@ -645,16 +753,27 @@ class _MessageInputState extends State {
final attachment = _SendingAttachment(
file: file,
- type: type,
+ attachment: Attachment(
+ localUri: file.uri,
+ type: attachmentType,
+ ),
);
setState(() {
_attachments.add(attachment);
});
- final url = await _uploadAttachment(file, type, channel);
+ final url = await _uploadAttachment(file, fileType, channel);
- attachment.url = url;
+ if (fileType == DefaultAttachmentTypes.image) {
+ attachment.attachment = attachment.attachment.copyWith(
+ imageUrl: url,
+ );
+ } else {
+ attachment.attachment = attachment.attachment.copyWith(
+ assetUrl: url,
+ );
+ }
setState(() {
attachment.uploaded = true;
@@ -663,14 +782,22 @@ class _MessageInputState extends State {
Future _uploadAttachment(
File file,
- FileType type,
+ DefaultAttachmentTypes type,
Channel channel,
) async {
String url;
- if (type == FileType.image) {
- url = await doImageUploadRequest(file, channel);
+ if (type == DefaultAttachmentTypes.image) {
+ if (widget.doImageUploadRequest != null) {
+ url = await widget.doImageUploadRequest(file, channel);
+ } else {
+ url = await _uploadImage(file, channel);
+ }
} else {
- url = await doFileUploadRequest(file, channel);
+ if (widget.doFileUploadRequest != null) {
+ url = await widget.doFileUploadRequest(file, channel);
+ } else {
+ url = await _uploadFile(file, channel);
+ }
}
return url;
}
@@ -714,25 +841,27 @@ class _MessageInputState extends State {
child: IconButton(
key: Key('sendButton'),
onPressed: () {
- _sendMessage(context);
+ sendMessage();
},
icon: Icon(
Icons.send,
+ color: StreamChatTheme.of(context).accentColor,
),
),
),
);
}
- void _sendMessage(BuildContext context) {
- final text = _textController.text.trim();
+ /// Sends the current message
+ void sendMessage() async {
+ final text = textEditingController.text.trim();
if (text.isEmpty && _attachments.isEmpty) {
return;
}
final attachments = List<_SendingAttachment>.from(_attachments);
- _textController.clear();
+ textEditingController.clear();
_attachments.clear();
setState(() {
@@ -751,21 +880,11 @@ class _MessageInputState extends State {
Message message;
if (widget.editMessage != null) {
message = widget.editMessage.copyWith(
- parentId: widget.parentMessage?.id,
text: text,
- attachments: widget.editMessage.attachments
- .where((attachment) => attachment.type == 'giphy')
- .toList() +
- _getAttachments(attachments).toList(),
+ attachments: _getAttachments(attachments).toList(),
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
);
-
- if (widget.editMessage.status == MessageSendingStatus.FAILED) {
- sendingFuture = channel.sendMessage(message);
- }
-
- sendingFuture = StreamChat.of(context).client.updateMessage(message);
} else {
message = (widget.initialMessage ?? Message()).copyWith(
parentId: widget.parentMessage?.id,
@@ -774,34 +893,36 @@ class _MessageInputState extends State {
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
);
- sendingFuture = channel.sendMessage(message);
}
- sendingFuture.whenComplete(() {
+ if (widget.preMessageSending != null) {
+ message = await widget.preMessageSending(message);
+ }
+
+ if (widget.editMessage == null ||
+ widget.editMessage.status == MessageSendingStatus.FAILED) {
+ sendingFuture = channel.sendMessage(message);
+ } else {
+ sendingFuture = StreamChat.of(context).client.updateMessage(
+ message,
+ channel.cid,
+ );
+ }
+
+ return sendingFuture.whenComplete(() {
if (widget.onMessageSent != null) {
widget.onMessageSent(message);
+ } else {
+ if (widget.editMessage != null) {
+ Navigator.pop(context);
+ }
}
});
}
Iterable _getAttachments(List<_SendingAttachment> attachments) {
return attachments.map((attachment) {
- String type;
- switch (attachment.type) {
- case FileType.image:
- type = 'image';
- break;
- case FileType.video:
- type = 'video';
- break;
- default:
- type = 'file';
- }
- return Attachment(
- imageUrl: attachment.type == FileType.image ? attachment.url : null,
- assetUrl: attachment.url,
- type: type,
- );
+ return attachment.attachment;
});
}
@@ -811,12 +932,10 @@ class _MessageInputState extends State {
void initState() {
super.initState();
- StreamChannel.of(context).queryMembersAndWatchers();
-
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
if (visible) {
if (_commandsOverlay != null) {
- if (_textController.text.startsWith('/')) {
+ if (textEditingController.text.startsWith('/')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
@@ -825,7 +944,7 @@ class _MessageInputState extends State {
}
if (_mentionsOverlay != null) {
- if (_textController.text.contains('@')) {
+ if (textEditingController.text.contains('@')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_mentionsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_mentionsOverlay);
@@ -842,44 +961,24 @@ class _MessageInputState extends State {
}
});
- if (widget.editMessage != null) {
- _parseExistingMessage(widget.editMessage);
- } else if (widget.initialMessage != null) {
- _parseExistingMessage(widget.initialMessage);
- } else {
- _textController = TextEditingController();
+ textEditingController =
+ widget.textEditingController ?? TextEditingController();
+ if (widget.editMessage != null || widget.initialMessage != null) {
+ _parseExistingMessage(widget.editMessage ?? widget.initialMessage);
}
}
void _parseExistingMessage(Message message) {
- _textController = TextEditingController(text: message.text);
+ textEditingController.text = message.text;
_typingStarted = true;
_messageIsPresent = true;
message.attachments?.forEach((attachment) {
- if (attachment.type == 'image') {
- _attachments.add(_SendingAttachment(
- type: FileType.image,
- url: attachment.imageUrl ??
- attachment.assetUrl ??
- attachment.thumbUrl ??
- attachment.ogScrapeUrl,
- uploaded: true,
- ));
- } else if (attachment.type == 'video') {
- _attachments.add(_SendingAttachment(
- type: FileType.video,
- url: attachment.assetUrl,
- uploaded: true,
- ));
- } else if (attachment.type != 'giphy') {
- _attachments.add(_SendingAttachment(
- type: FileType.any,
- url: attachment.assetUrl,
- uploaded: true,
- ));
- }
+ _attachments.add(_SendingAttachment(
+ attachment: attachment,
+ uploaded: true,
+ ));
});
}
@@ -903,15 +1002,13 @@ class _MessageInputState extends State {
}
class _SendingAttachment {
- final File file;
- final FileType type;
- String url;
+ File file;
+ Attachment attachment;
bool uploaded;
_SendingAttachment({
- this.url,
this.file,
- this.type,
+ this.attachment,
this.uploaded = false,
});
}
diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart
index 13ea7233..a48fee83 100644
--- a/lib/src/message_list_view.dart
+++ b/lib/src/message_list_view.dart
@@ -3,17 +3,56 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/message_widget.dart';
+import 'package:stream_chat_flutter/src/system_message.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../stream_chat_flutter.dart';
-import 'message_widget.dart';
+import 'date_divider.dart';
import 'stream_channel.dart';
-typedef MessageBuilder = Widget Function(BuildContext, Message, int index);
-typedef ParentMessageBuilder = Widget Function(BuildContext, Message);
+typedef MessageBuilder = Widget Function(
+ BuildContext,
+ MessageDetails,
+ List,
+);
+typedef ParentMessageBuilder = Widget Function(
+ BuildContext,
+ Message,
+);
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
typedef ThreadTapCallback = void Function(Message, Widget);
+class MessageDetails {
+ /// True if the message belongs to the current user
+ bool isMyMessage;
+
+ /// True if the user message is the same of the previous message
+ bool isLastUser;
+
+ /// True if the user message is the same of the next message
+ bool isNextUser;
+
+ /// The message
+ Message message;
+
+ /// The index of the message
+ int index;
+
+ MessageDetails(
+ BuildContext context,
+ this.message,
+ List messages,
+ this.index,
+ ) {
+ isMyMessage = message.user.id == StreamChat.of(context).user.id;
+ isLastUser = index + 1 < messages.length &&
+ message.user.id == messages[index + 1]?.user?.id;
+ isNextUser =
+ index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
+ }
+}
+
/// 
/// 
///
@@ -55,6 +94,7 @@ typedef ThreadTapCallback = void Function(Message, Widget);
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageListView extends StatefulWidget {
+ /// Instantiate a new MessageListView
MessageListView({
Key key,
this.messageBuilder,
@@ -62,11 +102,8 @@ class MessageListView extends StatefulWidget {
this.parentMessage,
this.threadBuilder,
this.onThreadTap,
- this.showOtherMessageUsername = false,
- this.showVideoFullScreen = true,
- this.onMentionTap,
- this.onUserAvatarTap,
- this.onMessageActions,
+ this.dateDividerBuilder,
+ this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
}) : super(key: key);
/// Function used to build a custom message widget
@@ -85,20 +122,11 @@ class MessageListView extends StatefulWidget {
/// Parent message in case of a thread
final Message parentMessage;
- /// If true show the other users username next to the timestamp of the message
- final bool showOtherMessageUsername;
+ /// Builder used to render date dividers
+ final Widget Function(DateTime) dateDividerBuilder;
- /// True if the video player will allow fullscreen mode
- final bool showVideoFullScreen;
-
- /// Function called on message mention tap
- final void Function(User) onMentionTap;
-
- /// Function called on User Avatar tap
- final void Function(User) onUserAvatarTap;
-
- /// Function called on message long press
- final Function(BuildContext, Message) onMessageActions;
+ /// The ScrollPhysics used by the ListView
+ final ScrollPhysics scrollPhysics;
@override
_MessageListViewState createState() => _MessageListViewState();
@@ -130,7 +158,7 @@ class _MessageListViewState extends State {
},
child: ListView.custom(
key: Key('messageListView'),
- physics: AlwaysScrollableScrollPhysics(),
+ physics: widget.scrollPhysics,
controller: _scrollController,
reverse: true,
childrenDelegate: SliverChildBuilderDelegate(
@@ -146,21 +174,7 @@ class _MessageListViewState extends State {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- MessageWidget(
- key: ValueKey(
- 'PARENT-MESSAGE-${widget.parentMessage.id}'),
- previousMessage: null,
- message: widget.parentMessage,
- nextMessage: null,
- onThreadTap: _onThreadTap,
- isParent: true,
- showVideoFullScreen: widget.showVideoFullScreen,
- showOtherMessageUsername:
- widget.showOtherMessageUsername,
- onMentionTap: widget.onMentionTap,
- onUserAvatarTap: widget.onUserAvatarTap,
- onMessageActions: widget.onMessageActions,
- ),
+ buildParentMessage(widget.parentMessage),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Container(
@@ -184,124 +198,58 @@ class _MessageListViewState extends State {
return _buildLoadingIndicator(streamChannel);
}
final message = _messages[i];
-
- final previousMessage =
- i < _messages.length - 1 ? _messages[i + 1] : null;
final nextMessage = i > 0 ? _messages[i - 1] : null;
Widget messageWidget;
if (i == 0) {
messageWidget = _buildBottomMessage(
- streamChannel,
- previousMessage,
- message,
context,
+ message,
+ _messages,
+ streamChannel,
);
} else if (i == _messages.length - 1) {
messageWidget = _buildTopMessage(
- message,
- nextMessage,
- streamChannel,
context,
+ message,
+ _messages,
+ streamChannel,
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey('MESSAGE-${message.id}'),
- builder: (_) => widget.messageBuilder(context, message, i),
+ builder: (_) => widget.messageBuilder(
+ context,
+ MessageDetails(
+ context,
+ message,
+ _messages,
+ i,
+ ),
+ _messages),
);
} else {
- messageWidget = MessageWidget(
- key: ValueKey('MESSAGE-${message.id}'),
- previousMessage: previousMessage,
- message: message,
- nextMessage: nextMessage,
- onThreadTap: _onThreadTap,
- showOtherMessageUsername: widget.showOtherMessageUsername,
- showVideoFullScreen: widget.showVideoFullScreen,
- onMentionTap: widget.onMentionTap,
- onUserAvatarTap: widget.onUserAvatarTap,
- onMessageActions: widget.onMessageActions,
- );
+ messageWidget = buildMessage(message, _messages, i);
}
}
if (nextMessage != null &&
!Jiffy(message.createdAt.toLocal())
.isSame(nextMessage.createdAt.toLocal(), Units.DAY)) {
- final divider = Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: Divider(),
- ),
- );
-
- final createdAt = Jiffy(nextMessage.createdAt.toLocal());
- final now = DateTime.now();
- final hourInfo = createdAt.format('h:mm a');
-
- String dayInfo;
- if (Jiffy(createdAt).isSame(now, Units.DAY)) {
- dayInfo = 'TODAY';
- } else if (Jiffy(createdAt)
- .isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
- dayInfo = 'YESTERDAY';
- } else if (Jiffy(createdAt).isAfter(
- now.subtract(Duration(days: 7)),
- Units.DAY,
- )) {
- dayInfo = createdAt.format('EEEE').toUpperCase();
- } else if (Jiffy(createdAt).isAfter(
- Jiffy(now).subtract(years: 1),
- Units.DAY,
- )) {
- dayInfo = createdAt.format('dd/MM').toUpperCase();
- } else {
- dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
- }
-
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
messageWidget,
Padding(
- padding: const EdgeInsets.only(top: 24.0),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- divider,
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32.0),
- child: Text.rich(
- TextSpan(
- children: [
- TextSpan(
- text: dayInfo,
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
- ),
- TextSpan(text: ' AT'),
- TextSpan(text: ' $hourInfo'),
- ],
- style: TextStyle(
- fontWeight: FontWeight.normal,
- ),
- ),
- style: TextStyle(
- fontSize: 10,
- color: Theme.of(context)
- .textTheme
- .title
- .color
- .withOpacity(.5),
- ),
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ child: widget.dateDividerBuilder != null
+ ? widget
+ .dateDividerBuilder(nextMessage.createdAt.toLocal())
+ : DateDivider(
+ dateTime: nextMessage.createdAt.toLocal(),
),
- ),
- divider,
- ],
- ),
),
],
);
@@ -329,9 +277,11 @@ class _MessageListViewState extends State {
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
- print((snapshot.error as Error).stackTrace.toString());
- return Center(
- child: Text(snapshot.error.toString()),
+ return Container(
+ color: Color(0xffd0021B).withAlpha(26),
+ child: Center(
+ child: Text('Error loading messages'),
+ ),
);
}
if (!snapshot.data) {
@@ -348,30 +298,28 @@ class _MessageListViewState extends State {
}
Widget _buildTopMessage(
- Message message,
- Message nextMessage,
- StreamChannelState streamChannelState,
BuildContext context,
+ Message message,
+ List messages,
+ StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
- key: ValueKey('MESSAGE-${message.id}'),
- builder: (_) => widget.messageBuilder(context, message, 0),
+ key: ValueKey('TOP-MESSAGE'),
+ builder: (_) => widget.messageBuilder(
+ context,
+ MessageDetails(
+ context,
+ message,
+ _messages,
+ _messages.length - 1,
+ ),
+ _messages,
+ ),
);
} else {
- messageWidget = MessageWidget(
- key: ValueKey('MESSAGE-${message.id}'),
- previousMessage: null,
- message: message,
- nextMessage: nextMessage,
- onThreadTap: _onThreadTap,
- showVideoFullScreen: widget.showVideoFullScreen,
- showOtherMessageUsername: widget.showOtherMessageUsername,
- onMentionTap: widget.onMentionTap,
- onUserAvatarTap: widget.onUserAvatarTap,
- onMessageActions: widget.onMessageActions,
- );
+ messageWidget = buildMessage(message, messages, _messages.length - 1);
}
return VisibilityDetector(
@@ -380,7 +328,7 @@ class _MessageListViewState extends State {
onVisibilityChanged: (visibility) {
final topIsVisible = visibility.visibleBounds != Rect.zero;
if (topIsVisible && !_topWasVisible) {
- streamChannelState.queryMessages();
+ streamChannel.queryMessages();
}
_topWasVisible = topIsVisible;
},
@@ -388,37 +336,35 @@ class _MessageListViewState extends State {
}
Widget _buildBottomMessage(
- StreamChannelState streamChannel,
- Message previousMessage,
- Message message,
BuildContext context,
+ Message message,
+ List messages,
+ StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
- key: ValueKey('MESSAGE-${message.id}'),
- builder: (_) => widget.messageBuilder(context, message, 0),
+ key: ValueKey('BOTTOM-MESSAGE'),
+ builder: (_) => widget.messageBuilder(
+ context,
+ MessageDetails(
+ context,
+ message,
+ _messages,
+ 0,
+ ),
+ _messages,
+ ),
);
} else {
- messageWidget = MessageWidget(
- key: ValueKey('MESSAGE-${message.id}'),
- previousMessage: previousMessage,
- message: message,
- nextMessage: null,
- onThreadTap: _onThreadTap,
- showVideoFullScreen: widget.showVideoFullScreen,
- showOtherMessageUsername: widget.showOtherMessageUsername,
- onMentionTap: widget.onMentionTap,
- onUserAvatarTap: widget.onUserAvatarTap,
- onMessageActions: widget.onMessageActions,
- );
+ messageWidget = buildMessage(message, messages, 0);
}
return VisibilityDetector(
key: ValueKey('BOTTOM-MESSAGE'),
onVisibilityChanged: (visibility) {
_isBottom = visibility.visibleBounds != Rect.zero;
- if (_isBottom && streamChannel.channel.config.readEvents) {
+ if (_isBottom && streamChannel.channel.config?.readEvents == true) {
if (streamChannel.channel.state.unreadCount > 0) {
streamChannel.channel.markRead();
}
@@ -428,6 +374,103 @@ class _MessageListViewState extends State {
);
}
+ Widget buildParentMessage(
+ Message message,
+ ) {
+ final isMyMessage = message.user.id == StreamChat.of(context).user.id;
+
+ return MessageWidget(
+ showReplyIndicator: false,
+ message: message,
+ reverse: isMyMessage,
+ showUsername: !isMyMessage,
+ padding: EdgeInsets.only(
+ top: 8.0,
+ left: 8.0,
+ right: 8.0,
+ bottom: 16.0,
+ ),
+ showSendingIndicator: DisplayWidget.hide,
+ onThreadTap: _onThreadTap,
+ showEditMessage: false,
+ showDeleteMessage: false,
+ borderRadiusGeometry: BorderRadius.only(
+ topLeft: Radius.circular(16),
+ bottomLeft: Radius.circular(2),
+ topRight: Radius.circular(16),
+ bottomRight: Radius.circular(16),
+ ),
+ borderSide: isMyMessage ? BorderSide.none : null,
+ showUserAvatar: DisplayWidget.show,
+ messageTheme: isMyMessage
+ ? StreamChatTheme.of(context).ownMessageTheme
+ : StreamChatTheme.of(context).otherMessageTheme,
+ );
+ }
+
+ Widget buildMessage(
+ Message message,
+ List messages,
+ int index,
+ ) {
+ if (message.type == 'system' && message.text?.isNotEmpty == true) {
+ return SystemMessage(
+ message: message,
+ );
+ }
+
+ final userId = StreamChat.of(context).user.id;
+ final isMyMessage = message.user.id == userId;
+ final isLastUser = index + 1 < messages.length &&
+ message.user.id == messages[index + 1]?.user?.id;
+ final isNextUser =
+ index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
+
+ final readList = StreamChannel.of(context)
+ .channel
+ .state
+ ?.read
+ ?.where((element) => element.user.id != userId)
+ ?.where((read) =>
+ read.lastRead.isAfter(message.createdAt) &&
+ (index == 0 ||
+ read.lastRead.isBefore(messages[index - 1].createdAt)))
+ ?.toList();
+
+ return MessageWidget(
+ message: message,
+ reverse: isMyMessage,
+ showReactions: !message.isDeleted,
+ padding: EdgeInsets.only(
+ left: 8.0,
+ right: 8.0,
+ bottom: index == 0 ? 30 : (isNextUser ? 5 : 10),
+ ),
+ showUsername: !isMyMessage && !isNextUser,
+ showSendingIndicator: isMyMessage &&
+ (index == 0 || message.status != MessageSendingStatus.SENT)
+ ? DisplayWidget.show
+ : DisplayWidget.hide,
+ showTimestamp: !isNextUser || readList?.isNotEmpty == true,
+ showEditMessage: isMyMessage,
+ showDeleteMessage: isMyMessage,
+ borderSide: isMyMessage ? BorderSide.none : null,
+ onThreadTap: _onThreadTap,
+ attachmentBorderRadiusGeometry: BorderRadius.circular(16),
+ borderRadiusGeometry: BorderRadius.only(
+ topLeft: Radius.circular(isLastUser ? 2 : 16),
+ bottomLeft: Radius.circular(2),
+ topRight: Radius.circular(16),
+ bottomRight: Radius.circular(16),
+ ),
+ showUserAvatar: isNextUser ? DisplayWidget.hide : DisplayWidget.show,
+ messageTheme: isMyMessage
+ ? StreamChatTheme.of(context).ownMessageTheme
+ : StreamChatTheme.of(context).otherMessageTheme,
+ readList: readList,
+ );
+ }
+
StreamSubscription _streamListener;
@override
@@ -442,11 +485,7 @@ class _MessageListViewState extends State {
Stream> stream;
if (widget.parentMessage == null) {
- stream = streamChannel.channel.state.messagesStream.map((messages) =>
- messages
- .where((m) =>
- !(m.status == MessageSendingStatus.FAILED && m.isDeleted))
- .toList());
+ stream = streamChannel.channel.state.messagesStream;
} else {
streamChannel.getReplies(widget.parentMessage.id);
stream = streamChannel.channel.state.threadsStream
@@ -454,7 +493,14 @@ class _MessageListViewState extends State {
.map((threads) => threads[widget.parentMessage.id]);
}
- _streamListener = stream.listen((newMessages) {
+ _streamListener = stream
+ .map((messages) =>
+ messages
+ ?.where((m) =>
+ !(m.status == MessageSendingStatus.FAILED && m.isDeleted))
+ ?.toList() ??
+ [])
+ .listen((newMessages) {
newMessages = newMessages.reversed.toList();
if (_messages.isEmpty ||
newMessages.isEmpty ||
diff --git a/lib/src/message_text.dart b/lib/src/message_text.dart
new file mode 100644
index 00000000..f35b8549
--- /dev/null
+++ b/lib/src/message_text.dart
@@ -0,0 +1,64 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+import 'stream_chat_theme.dart';
+import 'utils.dart';
+
+class MessageText extends StatelessWidget {
+ const MessageText({
+ Key key,
+ @required this.message,
+ @required this.messageTheme,
+ this.onMentionTap,
+ }) : super(key: key);
+
+ final Message message;
+ final void Function(User) onMentionTap;
+ final MessageTheme messageTheme;
+
+ @override
+ Widget build(BuildContext context) {
+ final text = _replaceMentions(message.text);
+ return MarkdownBody(
+ data: text,
+ onTapLink: (link) {
+ if (link.startsWith('@')) {
+ final mentionedUser = message.mentionedUsers.firstWhere(
+ (u) => '@${u.name.replaceAll(' ', '')}' == link,
+ orElse: () => null,
+ );
+
+ if (onMentionTap != null) {
+ onMentionTap(mentionedUser);
+ } else {
+ print('tap on ${mentionedUser.name}');
+ }
+ } else {
+ launchURL(context, link);
+ }
+ },
+ styleSheet: MarkdownStyleSheet.fromTheme(
+ Theme.of(context).copyWith(
+ textTheme: Theme.of(context).textTheme.apply(
+ bodyColor: messageTheme.messageText.color,
+ decoration: messageTheme.messageText.decoration,
+ decorationColor: messageTheme.messageText.decorationColor,
+ decorationStyle: messageTheme.messageText.decorationStyle,
+ fontFamily: messageTheme.messageText.fontFamily,
+ ),
+ ),
+ ).copyWith(
+ p: messageTheme.messageText,
+ ),
+ );
+ }
+
+ String _replaceMentions(String text) {
+ message.mentionedUsers?.forEach((u) {
+ text = text.replaceAll(
+ '@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
+ });
+ return text;
+ }
+}
diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart
index 5445fc59..3294ab17 100644
--- a/lib/src/message_widget.dart
+++ b/lib/src/message_widget.dart
@@ -1,23 +1,29 @@
import 'dart:math';
+import 'dart:ui';
-import 'package:cached_network_image/cached_network_image.dart';
-import 'package:chewie/chewie.dart';
-import 'package:flutter/foundation.dart';
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
-import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_portal/flutter_portal.dart';
import 'package:jiffy/jiffy.dart';
-import 'package:stream_chat/stream_chat.dart';
-import 'package:stream_chat_flutter/src/message_input.dart';
-import 'package:stream_chat_flutter/src/message_list_view.dart';
-import 'package:stream_chat_flutter/src/reaction_picker.dart';
-import 'package:stream_chat_flutter/src/stream_channel.dart';
-import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
-import 'package:stream_chat_flutter/src/user_avatar.dart';
-import 'package:url_launcher/url_launcher.dart';
-import 'package:video_player/video_player.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
-import 'stream_chat.dart';
+import 'message_actions_bottom_sheet.dart';
+import 'message_text.dart';
+
+typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
+
+/// The display behaviour of a widget
+enum DisplayWidget {
+ /// Hides the widget replacing its space with a spacer
+ hide,
+
+ /// Hides the widget not replacing its space
+ gone,
+
+ /// Shows the widget normally
+ show,
+}
/// 
/// 
@@ -29,654 +35,469 @@ import 'stream_chat.dart';
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageWidget extends StatefulWidget {
- const MessageWidget({
- Key key,
- @required this.previousMessage,
- @required this.message,
- @required this.nextMessage,
- this.onThreadTap,
- this.onUserAvatarTap,
- this.onMessageActions,
- this.isParent = false,
- this.onMentionTap,
- this.showOtherMessageUsername = false,
- this.showVideoFullScreen = true,
- }) : super(key: key);
-
/// Function called on mention tap
final void Function(User) onMentionTap;
- /// Function called on long press
- final Function(BuildContext, Message) onMessageActions;
-
- /// If true show the other users username next to the timestamp of the message
- final bool showOtherMessageUsername;
-
- /// This message
- final Message message;
-
- /// The previous message
- final Message previousMessage;
-
- /// The next message
- final Message nextMessage;
-
/// The function called when tapping on replies
final void Function(Message) onThreadTap;
+ final Widget Function(BuildContext, Message) editMessageInputBuilder;
+ final Widget Function(BuildContext, Message) textBuilder;
+
+ /// Function called on long press
+ final void Function(BuildContext, Message) onMessageActions;
+
+ /// The message
+ final Message message;
+
+ /// The message theme
+ final MessageTheme messageTheme;
+
+ /// If true the widget will be mirrored
+ final bool reverse;
+
+ /// The shape of the message text
+ final ShapeBorder shape;
+
+ /// The shape of an attachment
+ final ShapeBorder attachmentShape;
+
+ /// The borderside of the message text
+ final BorderSide borderSide;
+
+ /// The borderside of an attachment
+ final BorderSide attachmentBorderSide;
+
+ /// The border radius of the message text
+ final BorderRadiusGeometry borderRadiusGeometry;
+
+ /// The border radius of an attachment
+ final BorderRadiusGeometry attachmentBorderRadiusGeometry;
+
+ /// The padding of the widget
+ final EdgeInsetsGeometry padding;
+
+ /// The internal padding of the message text
+ final EdgeInsetsGeometry textPadding;
+
+ /// The internal padding of an attachment
+ final EdgeInsetsGeometry attachmentPadding;
+
+ /// It controls the display behaviour of the user avatar
+ final DisplayWidget showUserAvatar;
+
+ /// It controls the display behaviour of the sending indicator
+ final DisplayWidget showSendingIndicator;
+
+ /// If true the widget will show the reactions
+ final bool showReactions;
+
+ /// If true the widget will show the reply indicator
+ final bool showReplyIndicator;
/// The function called when tapping on UserAvatar
final void Function(User) onUserAvatarTap;
- /// True if this is the parent of the thread being showed
- final bool isParent;
+ final List readList;
- /// True if the video player will allow fullscreen mode
- final bool showVideoFullScreen;
+ /// If true show the users username next to the timestamp of the message
+ final bool showUsername;
+ final bool showTimestamp;
+ final bool showDeleteMessage;
+ final bool showEditMessage;
+ final Map attachmentBuilders;
+
+ MessageWidget({
+ Key key,
+ @required this.message,
+ @required this.messageTheme,
+ this.reverse = false,
+ this.shape,
+ this.attachmentShape,
+ this.borderSide,
+ this.attachmentBorderSide,
+ this.borderRadiusGeometry,
+ this.attachmentBorderRadiusGeometry,
+ this.onMentionTap,
+ this.showUserAvatar = DisplayWidget.show,
+ this.showSendingIndicator = DisplayWidget.show,
+ this.showReplyIndicator = true,
+ this.onThreadTap,
+ this.showUsername = true,
+ this.showTimestamp = true,
+ this.showReactions = true,
+ this.showDeleteMessage = true,
+ this.showEditMessage = true,
+ this.onUserAvatarTap,
+ this.onMessageActions,
+ this.editMessageInputBuilder,
+ this.textBuilder,
+ Map customAttachmentBuilders,
+ this.readList,
+ this.padding,
+ this.textPadding = const EdgeInsets.all(8.0),
+ this.attachmentPadding = EdgeInsets.zero,
+ }) : attachmentBuilders = {
+ 'image': (context, message, attachment) {
+ return ImageAttachment(
+ attachment: attachment,
+ message: message,
+ messageTheme: messageTheme,
+ size: Size(
+ MediaQuery.of(context).size.width * 0.8,
+ MediaQuery.of(context).size.height * 0.3,
+ ),
+ );
+ },
+ 'video': (context, message, attachment) {
+ return VideoAttachment(
+ attachment: attachment,
+ messageTheme: messageTheme,
+ size: Size(
+ MediaQuery.of(context).size.width * 0.8,
+ MediaQuery.of(context).size.height * 0.3,
+ ),
+ );
+ },
+ 'giphy': (context, message, attachment) {
+ return GiphyAttachment(
+ attachment: attachment,
+ messageTheme: messageTheme,
+ message: message,
+ size: Size(
+ MediaQuery.of(context).size.width * 0.8,
+ MediaQuery.of(context).size.height * 0.3,
+ ),
+ );
+ },
+ 'file': (context, message, attachment) {
+ return FileAttachment(
+ attachment: attachment,
+ size: Size(
+ MediaQuery.of(context).size.width * 0.8,
+ MediaQuery.of(context).size.height * 0.3,
+ ),
+ );
+ },
+ }..addAll(customAttachmentBuilders ?? {}),
+ super(key: key);
@override
_MessageWidgetState createState() => _MessageWidgetState();
}
-class _MessageWidgetState extends State
- with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
- final Map _videoControllers = {};
- final Map _chuwieControllers = {};
+class _MessageWidgetState extends State {
+ final Map _reactionToEmoji = {
+ 'love': '❤️️',
+ 'haha': '😂',
+ 'like': '👍',
+ 'sad': '😕',
+ 'angry': '😡',
+ 'wow': '😲',
+ };
- MessageTheme _messageTheme;
- StreamChatState _streamChat;
- StreamChannelState _streamChannel;
- bool _isMyMessage;
-
- String _currentUserId;
- String _messageUserId;
- String _previousUserId;
- String _nextUserId;
- bool _isLastUser;
- bool _isNextUser;
+ final GlobalKey _reactionPickerKey = GlobalKey();
+ double _reactionPadding = 0;
@override
Widget build(BuildContext context) {
- super.build(context);
-
- _messageTheme = _isMyMessage
- ? StreamChatTheme.of(context).ownMessageTheme
- : StreamChatTheme.of(context).otherMessageTheme;
-
- final alignment =
- _isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
-
- var row = [
- Column(
- crossAxisAlignment:
- _isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
- children: [
- widget.message.isDeleted
- ? _buildDeletedMessage(alignment)
- : _buildBubble(context),
- if (_streamChannel.channel.config.replies)
- _buildThreadIndicator(context),
- if (!_isNextUser) _buildTimestamp(alignment),
- ],
- ),
- _isNextUser
- ? Container(
- width: 40,
- )
- : _buildUserAvatar(),
- ];
-
- if (!_isMyMessage) {
- row = row.reversed.toList();
+ var leftPadding = widget.showUserAvatar != DisplayWidget.gone
+ ? widget.messageTheme.avatarTheme.constraints.maxWidth + 23.0
+ : 12.0;
+ if (widget.showSendingIndicator == DisplayWidget.gone) {
+ leftPadding -= 7;
}
-
- return Container(
- padding: EdgeInsets.symmetric(
- horizontal: (_isMyMessage && widget.nextMessage == null) ? 0.0 : 10,
- ),
- margin: EdgeInsets.only(
- top: _isLastUser ? 5 : 24,
- bottom: widget.nextMessage == null ? 30 : 0,
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- mainAxisAlignment:
- _isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
- mainAxisSize: MainAxisSize.max,
- children: row,
+ return Portal(
+ child: Padding(
+ padding: widget.padding ?? EdgeInsets.all(8),
+ child: Transform(
+ alignment: Alignment.center,
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ child: Container(
+ alignment: Alignment.centerLeft,
+ child: Container(
+ constraints: BoxConstraints.loose(
+ Size.fromWidth(MediaQuery.of(context).size.width * 0.8),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IntrinsicWidth(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (widget.showSendingIndicator ==
+ DisplayWidget.show)
+ _buildSendingIndicator(),
+ SizedBox(
+ width: 2,
+ ),
+ if (widget.showSendingIndicator ==
+ DisplayWidget.hide)
+ SizedBox(
+ width: 8,
+ ),
+ if (widget.showUserAvatar == DisplayWidget.show)
+ _buildUserAvatar(),
+ SizedBox(
+ width: 6,
+ ),
+ if (widget.showUserAvatar == DisplayWidget.hide)
+ SizedBox(
+ width: widget.messageTheme.avatarTheme
+ .constraints.maxWidth +
+ 8,
+ ),
+ Flexible(
+ child: Padding(
+ padding: widget.showReactions
+ ? EdgeInsets.only(
+ top: _reactionPadding,
+ )
+ : EdgeInsets.zero,
+ child: PortalEntry(
+ portalAnchor: Alignment(0, 1),
+ childAnchor: Alignment.topRight,
+ portal: _buildReactionIndicator(context),
+ child: (widget.message.isDeleted &&
+ widget.message.status !=
+ MessageSendingStatus
+ .FAILED_DELETE)
+ ? Transform(
+ alignment: Alignment.center,
+ transform: Matrix4.rotationY(
+ widget.reverse ? pi : 0),
+ child: DeletedMessage(
+ messageTheme: widget.messageTheme,
+ ),
+ )
+ : Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ ..._parseAttachments(context),
+ if (widget.message.text
+ .trim()
+ .isNotEmpty)
+ _buildTextBubble(context),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (widget.showReplyIndicator &&
+ widget.message.replyCount > 0)
+ _buildReplyIndicator(leftPadding),
+ ],
+ ),
+ ),
+ if ((widget.message.createdAt != null &&
+ widget.showTimestamp) ||
+ widget.showUsername ||
+ widget.readList?.isNotEmpty == true)
+ _buildBottomRow(leftPadding),
+ ],
+ ),
+ ),
+ ),
+ ),
),
);
}
- Padding _buildUserAvatar() {
- return Padding(
- padding: EdgeInsets.only(
- left: _isMyMessage ? 8.0 : 0,
- right: _isMyMessage ? 0 : 8.0,
- ),
- child: Row(
- children: [
- UserAvatar(
- user: widget.message.user,
- onTap: widget.onUserAvatarTap,
- ),
- if (_isMyMessage &&
- widget.nextMessage == null &&
- (widget.message.status == MessageSendingStatus.SENT ||
- widget.message.status == null))
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 1.0,
- ),
- child: CircleAvatar(
- radius: 4,
- backgroundColor: Theme.of(context).accentColor,
- child: Icon(
- Icons.done,
- color: Colors.white,
- size: 4,
- ),
- ),
- ),
- if (_isMyMessage &&
- widget.message.status == MessageSendingStatus.SENDING)
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 1.0,
- ),
- child: CircleAvatar(
- radius: 4,
- backgroundColor: Colors.grey,
- child: Icon(
- Icons.access_time,
- size: 4,
- color: Colors.white,
- ),
- ),
- ),
- if (_isMyMessage &&
- widget.message.status == MessageSendingStatus.FAILED)
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 1.0,
- ),
- child: CircleAvatar(
- radius: 4,
- backgroundColor: Color(0xffd0021B).withOpacity(.1),
- child: Icon(
- Icons.error_outline,
- size: 4,
- color: Colors.white,
- ),
- ),
- ),
- ],
- ),
- );
+ @override
+ void didUpdateWidget(MessageWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ _updateReactionPadding();
}
@override
void initState() {
super.initState();
-
- _streamChat = StreamChat.of(context);
- _streamChannel = StreamChannel.of(context);
-
- _currentUserId = _streamChat.client.state.user.id;
- _messageUserId = widget.message.user.id;
- _previousUserId = widget.previousMessage?.user?.id;
- _nextUserId = widget.nextMessage?.user?.id;
- _isLastUser = _previousUserId == _messageUserId;
- _isNextUser = _nextUserId == _messageUserId;
-
- _isMyMessage = _messageUserId == _currentUserId;
+ _updateReactionPadding();
}
- Align _buildDeletedMessage(Alignment alignment) {
- return Align(
- alignment: alignment,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 14,
- ),
- child: Text(
- 'This message was deleted...',
- style: _messageTheme.messageText.copyWith(
- fontStyle: FontStyle.italic,
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white
- : Colors.black,
- ),
- ),
- ),
- );
- }
-
- Widget _buildThreadIndicator(BuildContext context) {
- var row = [
- Text(
- 'Replies: ${widget.message.replyCount}',
- style: _messageTheme.replies,
- ),
- Transform(
- transform: Matrix4.rotationY(_isMyMessage ? 0 : pi),
- alignment: Alignment.center,
- child: Icon(
- Icons.subdirectory_arrow_left,
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white12
- : Colors.black12,
- ),
- ),
- ];
-
- if (!_isMyMessage) {
- row = row.reversed.toList();
- }
-
- return widget.message.replyCount > 0
- ? GestureDetector(
- onTap: () {
- if (widget.isParent) {
- return;
- }
- if (widget.onThreadTap != null) {
- widget.onThreadTap(widget.message);
- }
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 2.0),
- child: Row(
- children: row,
- ),
- ),
- )
- : SizedBox();
- }
-
- Widget _buildBubble(
- BuildContext context,
- ) {
- var nOfAttachmentWidgets = 0;
-
- final column =
- List.from(widget.message.attachments.map((attachment) {
- nOfAttachmentWidgets++;
-
- Widget attachmentWidget;
- if (attachment.type == 'video') {
- attachmentWidget = _buildVideo(attachment);
- } else if (attachment.type == 'image' || attachment.type == 'giphy') {
- attachmentWidget = _buildImage(attachment);
- } else if (attachment.type == 'file') {
- attachmentWidget = Material(
- child: InkWell(
- onTap: () {
- _launchURL(attachment.assetUrl);
- },
- child: Container(
- width: 100,
- height: 100,
- child: Center(
- child: Icon(Icons.attach_file),
- ),
- ),
- ),
- );
+ void _updateReactionPadding() {
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ if (!mounted) {
+ return;
}
-
- if (attachmentWidget != null) {
- return _buildAttachment(
- attachmentWidget,
- attachment,
- nOfAttachmentWidgets,
- context,
- );
+ if (_reactionPickerKey.currentContext != null &&
+ widget.message.reactionCounts != null &&
+ widget.message.reactionCounts.values
+ .where((element) => element > 0)
+ .isNotEmpty) {
+ setState(() {
+ _reactionPadding = _reactionPickerKey.currentContext.size.height;
+ });
+ } else {
+ setState(() {
+ _reactionPadding = 0;
+ });
}
+ });
+ }
- nOfAttachmentWidgets--;
- return SizedBox();
- }));
-
- if (widget.message.text.trim().isNotEmpty) {
- var text = widget.message.text;
- text = _replaceMentions(text);
-
- column.addAll(
- [
- Column(
- crossAxisAlignment: _isMyMessage
- ? CrossAxisAlignment.end
- : CrossAxisAlignment.start,
- children: [
- if (_streamChannel.channel.config.reactions &&
- nOfAttachmentWidgets == 0)
- Align(
- child: _buildReactions(),
- alignment: _isMyMessage
- ? Alignment.centerLeft
- : Alignment.centerRight,
+ Widget _buildReactionsTail(BuildContext context) {
+ return AnimatedSwitcher(
+ duration: Duration(milliseconds: 300),
+ child: widget.message.reactionCounts?.isNotEmpty == true
+ ? Transform.translate(
+ offset: Offset(4, 0),
+ child: CustomPaint(
+ painter: ReactionBubblePainter(
+ Theme.of(context).brightness == Brightness.dark
+ ? Colors.white
+ : Colors.black,
),
- Stack(
- overflow: Overflow.visible,
- children: [
- if (nOfAttachmentWidgets == 0) _buildReactionPaint(),
- _buildMessageText(nOfAttachmentWidgets, text, context),
- ],
),
- ],
- ),
- ],
- );
- }
-
- if (_streamChannel.channel.config.reactions && nOfAttachmentWidgets > 0) {
- column.insert(
- 0,
- Align(
- child: _buildReactions(),
- alignment:
- _isMyMessage ? Alignment.centerLeft : Alignment.centerRight,
- ),
- );
- column[1] = Stack(
- overflow: Overflow.visible,
- children: [
- Padding(
- padding: EdgeInsets.only(
- right: _isMyMessage ? 0.0 : 8.0,
- left: _isMyMessage ? 8.0 : 0.0,
- ),
- child: column[1],
- ),
- _buildReactionPaint(),
- ],
- );
- }
-
- return GestureDetector(
- child: IntrinsicWidth(
- child: Column(
- children: column,
- crossAxisAlignment:
- _isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
- ),
- ),
- onTap: () {
- if (widget.message.status == MessageSendingStatus.FAILED) {
- StreamChannel.of(context).channel.sendMessage(widget.message);
- return;
- }
- },
- onLongPress: () {
- if (widget.message.isEphemeral ||
- widget.message.status == MessageSendingStatus.SENDING) {
- return;
- }
-
- if (widget.onMessageActions != null) {
- widget.onMessageActions(context, widget.message);
- } else {
- _showMessageBottomSheet(context);
- }
- },
+ )
+ : SizedBox(),
);
}
- Padding _buildAttachment(
- Widget attachmentWidget,
- Attachment attachment,
- int nOfAttachmentWidgets,
- BuildContext context,
- ) {
- final boxDecoration = _buildBoxDecoration(_isLastUser);
- return Padding(
- padding: const EdgeInsets.only(bottom: 2.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- ClipRRect(
- borderRadius: boxDecoration.borderRadius,
- child: Container(
- decoration: boxDecoration,
- constraints: BoxConstraints.loose(
- Size.fromWidth(MediaQuery.of(context).size.width * 0.7),
- ),
- child: Stack(
- children: [
- Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- attachmentWidget,
- if (attachment.title != null)
- _buildAttachmentTitle(attachment),
- ],
- ),
- if (attachment.type == 'image' &&
- attachment.titleLink != null)
- _buildPreviewInkwell(attachment),
- ],
- ),
- margin: EdgeInsets.only(
- top: nOfAttachmentWidgets > 1 ? 5 : 0,
- ),
- ),
- ),
- if (attachment.actions != null)
- _buildAttachmentActions(attachment, context),
- ],
- ),
- );
- }
-
- Padding _buildMessageText(
- int nOfAttachmentWidgets,
- String text,
- BuildContext context,
- ) {
+ Padding _buildBottomRow(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
- right: _isMyMessage ? 0.0 : 8.0,
- left: _isMyMessage ? 8.0 : 0.0,
+ left: leftPadding,
+ top: 2,
),
- child: Container(
- decoration:
- _buildBoxDecoration(_isLastUser || nOfAttachmentWidgets > 0),
- padding: EdgeInsets.all(10),
- constraints: BoxConstraints.loose(
- Size.fromWidth(MediaQuery.of(context).size.width * 0.7)),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (widget.message.status == MessageSendingStatus.FAILED)
- Text(
- 'MESSAGE FAILED · CLICK TO TRY AGAIN',
- style: _messageTheme.messageText.copyWith(
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white.withOpacity(.5)
- : Colors.black.withOpacity(.5),
- fontSize: 11,
- ),
- ),
- MarkdownBody(
- data: text,
- onTapLink: (link) {
- if (link.startsWith('@')) {
- final mentionedUser =
- widget.message.mentionedUsers.firstWhere(
- (u) => '@${u.name.replaceAll(' ', '')}' == link,
- orElse: () => null,
- );
-
- if (widget.onMentionTap != null) {
- widget.onMentionTap(mentionedUser);
- } else {
- print('tap on ${mentionedUser.name}');
- }
- } else {
- _launchURL(link);
- }
- },
- styleSheet: MarkdownStyleSheet.fromTheme(
- Theme.of(context).copyWith(
- textTheme: Theme.of(context).textTheme.apply(
- bodyColor: _messageTheme.messageText.color,
- decoration: _messageTheme.messageText.decoration,
- decorationColor:
- _messageTheme.messageText.decorationColor,
- decorationStyle:
- _messageTheme.messageText.decorationStyle,
- fontFamily: _messageTheme.messageText.fontFamily,
- ),
- ),
- ).copyWith(
- p: _messageTheme.messageText,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Transform(
+ alignment: Alignment.center,
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ child: RichText(
+ text: TextSpan(
+ style: widget.messageTheme.createdAt,
+ children: [
+ if (widget.showUsername)
+ TextSpan(
+ text: widget.message.user.name,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ if (widget.message.createdAt != null && widget.showTimestamp)
+ TextSpan(
+ text: Jiffy(widget.message.createdAt.toLocal())
+ .format(' HH:mm'),
+ ),
+ ],
),
),
- ],
- ),
+ ),
+ if (widget.readList?.isNotEmpty == true)
+ SizedBox.fromSize(
+ size: Size((widget.readList.length * 10.0) + 10, 17),
+ child: Transform(
+ alignment: Alignment.center,
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ child: Padding(
+ padding: const EdgeInsets.only(left: 4.0),
+ child: _buildReadIndicator(),
+ ),
+ ),
+ ),
+ ],
),
);
}
- String _replaceMentions(String text) {
- widget.message.mentionedUsers?.forEach((u) {
- text = text.replaceAll(
- '@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
- });
- return text;
- }
-
- Row _buildAttachmentActions(Attachment attachment, BuildContext context) {
- return Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: attachment.actions?.map((action) {
- if (action.style == 'primary') {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4.0),
- child: FlatButton(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
+ Widget _buildReadIndicator() {
+ var padding = 0.0;
+ return Stack(
+ children: widget.readList.map((e) {
+ padding += 10.0;
+ return Positioned(
+ left: padding - 10,
+ bottom: 0,
+ top: 0,
+ child: Material(
+ color: Colors.white,
+ shape: CircleBorder(),
+ child: Padding(
+ padding: const EdgeInsets.all(1.0),
+ child: UserAvatar(
+ user: e.user,
+ constraints: BoxConstraints.loose(Size.fromRadius(16)),
),
- child: Text('${action.text}'),
- color: action.style == 'primary'
- ? StreamChatTheme.of(context).accentColor
- : null,
- textColor: Colors.white,
- onPressed: () {
- _streamChannel.channel.sendAction(widget.message.id, {
- action.name: action.value,
- });
- },
),
- );
- }
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4.0),
- child: OutlineButton(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- ),
- child: Text('${action.text}'),
- color: StreamChatTheme.of(context).accentColor,
- onPressed: () {
- _streamChannel.channel.sendAction(widget.message.id, {
- action.name: action.value,
- });
- },
),
);
- })?.toList(),
+ }).toList(),
);
}
- Positioned _buildPreviewInkwell(Attachment attachment) {
- return Positioned.fill(
- child: Material(
- color: Colors.transparent,
- child: InkWell(
- onTap: () => _launchURL(attachment.titleLink),
- ),
- ),
- );
- }
-
- GestureDetector _buildAttachmentTitle(Attachment attachment) {
- return GestureDetector(
- onTap: () {
- if (attachment.titleLink != null) {
- _launchURL(attachment.titleLink);
- }
- },
- child: Container(
- constraints: BoxConstraints.loose(
- Size(
- MediaQuery.of(context).size.width * 0.7,
- 500,
- ),
- ),
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- attachment.title,
- overflow: TextOverflow.ellipsis,
- style: _messageTheme.messageText.copyWith(
- color: StreamChatTheme.of(context).accentColor,
- fontWeight: FontWeight.bold,
+ Widget _buildReactionIndicator(BuildContext context) {
+ return AnimatedSwitcher(
+ key: _reactionPickerKey,
+ duration: Duration(milliseconds: 300),
+ child: (widget.showReactions &&
+ widget.message.reactionCounts?.isNotEmpty == true &&
+ !widget.message.isDeleted)
+ ? Container(
+ child: GestureDetector(
+ onTap: () => onLongPress(context),
+ child: Container(
+ width: MediaQuery.of(context).size.width * 0.3,
+ padding: const EdgeInsets.only(
+ bottom: 4.0,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color:
+ Theme.of(context).brightness == Brightness.dark
+ ? Colors.white
+ : Colors.black,
+ borderRadius: BorderRadius.all(Radius.circular(14)),
+ ),
+ child: _buildReactionsText(context),
+ ),
+ ),
+ _buildReactionsTail(context),
+ ],
+ ),
),
),
- if (attachment.titleLink != null ||
- attachment.ogScrapeUrl != null)
- Text(
- Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
- .authority
- .split('.')
- .reversed
- .take(2)
- .toList()
- .reversed
- .join('.'),
- overflow: TextOverflow.ellipsis,
- style: _messageTheme.createdAt,
- ),
- ],
- ),
- ),
- ),
+ )
+ : SizedBox(),
);
}
- Widget _buildReactionPaint() {
- return widget.message.reactionCounts?.isNotEmpty == true
- ? Positioned(
- left: _isMyMessage ? 8 : null,
- right: !_isMyMessage ? 8 : null,
- top: -6,
- child: CustomPaint(
- painter: _ReactionBubblePainter(
- Theme.of(context).brightness == Brightness.dark
- ? Colors.white
- : Colors.black,
- ),
- ),
- )
- : SizedBox();
+ Text _buildReactionsText(BuildContext context) {
+ return Text(
+ widget.message.reactionCounts.keys.map((reactionType) {
+ return _reactionToEmoji[reactionType] ?? '?';
+ }).join(' ') +
+ ' ${widget.message.reactionCounts.values.fold(0, (t, v) => v + t).toString()}',
+ style: TextStyle(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.black
+ : Colors.white,
+ ),
+ textAlign: TextAlign.justify,
+ );
}
void _showMessageBottomSheet(BuildContext context) {
- if (!_streamChannel.channel.config.reactions &&
- !_streamChannel.channel.config.replies) {
- return;
- }
-
- final theme = Theme.of(context);
-
+ final channel = StreamChannel.of(context).channel;
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
@@ -686,449 +507,292 @@ class _MessageWidgetState extends State
),
),
context: context,
- builder: (_) {
- return SafeArea(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Container(
- color: Colors.black87,
- child: (_streamChannel.channel.config.reactions &&
- widget.message.status != MessageSendingStatus.FAILED)
- ? ReactionPicker(
- channel: StreamChannel.of(context).channel,
- reactionToEmoji: reactionToEmoji,
- message: widget.message,
- )
- : SizedBox(),
- ),
- _isMyMessage
- ? FlatButton(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Text(
- 'Delete message',
- style: theme.textTheme.headline
- .copyWith(color: Colors.red),
- ),
- ),
- onPressed: () {
- Navigator.pop(context);
- StreamChat.of(context).client.deleteMessage(
- widget.message,
- _streamChannel.channel.cid,
- );
- },
- )
- : SizedBox(),
- _isMyMessage
- ? FlatButton(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Text(
- 'Edit message',
- style: theme.textTheme.headline,
- ),
- ),
- onPressed: () async {
- Navigator.pop(context);
-
- _showEditBottomSheet(context);
- },
- )
- : SizedBox(),
- (_streamChannel.channel.config.replies &&
- widget.message.status != MessageSendingStatus.FAILED &&
- widget.message.parentId == null &&
- !widget.isParent)
- ? FlatButton(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Text(
- 'Start a thread',
- style: theme.textTheme.headline,
- ),
- ),
- onPressed: () {
- Navigator.pop(context);
- widget.onThreadTap(widget.message);
- },
- )
- : SizedBox(),
- ],
+ builder: (context) {
+ return StreamChannel(
+ channel: channel,
+ child: MessageActionsBottomSheet(
+ showDeleteMessage: widget.showDeleteMessage,
+ message: widget.message,
+ editMessageInputBuilder: widget.editMessageInputBuilder,
+ onThreadTap: widget.onThreadTap,
+ showEditMessage: widget.showEditMessage,
+ showReactions: widget.showReactions,
+ showReply:
+ widget.showReplyIndicator && widget.onThreadTap != null,
),
);
});
}
- void _showEditBottomSheet(BuildContext context) {
- showModalBottomSheet(
- context: context,
- elevation: 2,
- clipBehavior: Clip.hardEdge,
- isScrollControlled: true,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.only(
- topLeft: Radius.circular(32),
- topRight: Radius.circular(32),
- ),
- ),
- builder: (context) {
- return StreamChannel(
- channel: _streamChannel.channel,
- child: Flex(
- direction: Axis.vertical,
- mainAxisAlignment: MainAxisAlignment.end,
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: const EdgeInsets.only(
- top: 16.0,
- left: 16.0,
- right: 16.0,
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- 'Edit message',
- style: Theme.of(context).textTheme.title,
- ),
- Container(
- height: 30,
- padding: const EdgeInsets.all(2.0),
- child: AspectRatio(
- aspectRatio: 1,
- child: RawMaterialButton(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(4),
- ),
- elevation: 0,
- highlightElevation: 0,
- focusElevation: 0,
- disabledElevation: 0,
- hoverElevation: 0,
- onPressed: () {
- Navigator.of(context).pop();
- },
- fillColor:
- Theme.of(context).brightness == Brightness.dark
- ? Colors.white.withOpacity(.1)
- : Colors.black.withOpacity(.1),
- padding: EdgeInsets.all(4),
- child: Icon(
- Icons.close,
- size: 15,
+ List _parseAttachments(BuildContext context) {
+ return widget.message.attachments?.map((attachment) {
+ final attachmentBuilder = widget.attachmentBuilders[attachment.type];
+
+ if (attachmentBuilder == null) {
+ return SizedBox();
+ }
+
+ return Padding(
+ padding: EdgeInsets.only(
+ bottom: 4,
+ ),
+ child: GestureDetector(
+ onTap: () => retryMessage(context),
+ onLongPress: () => onLongPress(context),
+ child: Material(
+ color: _getBackgroundColor(),
+ clipBehavior: Clip.hardEdge,
+ shape: widget.attachmentShape ??
+ widget.shape ??
+ ContinuousRectangleBorder(
+ side: widget.attachmentBorderSide ??
+ widget.borderSide ??
+ BorderSide(
color:
Theme.of(context).brightness == Brightness.dark
- ? Colors.white
- : Colors.black,
+ ? Colors.white.withAlpha(24)
+ : Colors.black.withAlpha(24),
),
- ),
- ),
+ borderRadius: widget.attachmentBorderRadiusGeometry ??
+ widget.borderRadiusGeometry ??
+ BorderRadius.zero,
),
- ],
+ child: Padding(
+ padding: widget.attachmentPadding,
+ child: Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ getFailedMessageWidget(
+ context,
+ padding: const EdgeInsets.all(8.0),
+ ),
+ attachmentBuilder(
+ context,
+ widget.message,
+ attachment,
+ ),
+ ],
+ ),
+ ),
),
),
- Padding(
- padding: EdgeInsets.only(
- bottom: MediaQuery.of(context).viewInsets.bottom,
- ),
- child: MessageInput(
- editMessage: widget.message,
- parentMessage: widget.isParent
- ? StreamChannel.of(context)
- .channel
- .state
- .messages
- .firstWhere((message) =>
- message.id == widget.message.parentId)
- : null,
- onMessageSent: (_) {
- FocusScope.of(context).unfocus();
- Navigator.pop(context);
- },
- ),
- ),
- ],
- ),
- );
- },
- );
- }
-
- Widget _buildReactions() {
- return GestureDetector(
- onTap: () {
- if (widget.onMessageActions != null) {
- widget.onMessageActions(context, widget.message);
- } else {
- _showMessageBottomSheet(context);
- }
- },
- child: Padding(
- padding: EdgeInsets.symmetric(
- vertical: widget.message.reactionCounts?.isNotEmpty == true ? 4.0 : 0,
- ),
- child: Container(
- padding: widget.message.reactionCounts?.isNotEmpty == true
- ? const EdgeInsets.all(8)
- : EdgeInsets.zero,
- decoration: BoxDecoration(
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white
- : Colors.black,
- borderRadius: BorderRadius.all(Radius.circular(14))),
- child: AnimatedSwitcher(
- duration: Duration(milliseconds: 300),
- reverseDuration: Duration(milliseconds: 0),
- child: (widget.message.reactionCounts != null &&
- widget.message.reactionCounts.isNotEmpty)
- ? _buildReactionRow()
- : SizedBox(),
- ),
- ),
- ),
- );
- }
-
- Row _buildReactionRow() {
- return Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- ...widget.message.reactionCounts.keys.map((reactionType) {
- return Text(
- reactionToEmoji[reactionType] ?? '?',
- );
- }),
- Padding(
- padding: const EdgeInsets.only(left: 4.0),
- child: Text(
- widget.message.reactionCounts.values
- .fold(0, (t, v) => v + t)
- .toString(),
- style: TextStyle(
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.black
- : Colors.white,
),
- ),
- ),
- ],
- );
+ );
+ })?.toList() ??
+ [];
}
- final Map reactionToEmoji = {
- 'love': '❤️️',
- 'haha': '😂',
- 'like': '👍',
- 'sad': '😕',
- 'angry': '😡',
- 'wow': '😲',
- };
-
- Widget _buildImage(
- Attachment attachment,
- ) {
- final errorWidget = Container(
- width: 200,
- height: 140,
- color: Color(0xffd0021B).withOpacity(.1),
- child: Center(
- child: Icon(
- Icons.error_outline,
- color: Colors.white,
- ),
- ),
- );
-
- if (attachment.thumbUrl == null &&
- attachment.imageUrl == null &&
- attachment.assetUrl == null) {
- return errorWidget;
+ void onLongPress(BuildContext context) {
+ if (widget.message.isEphemeral ||
+ widget.message.status == MessageSendingStatus.SENDING) {
+ return;
}
- return CachedNetworkImage(
- imageUrl:
- attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
- errorWidget: (context, url, error) {
- return errorWidget;
- },
- fit: BoxFit.cover,
- );
- }
-
- Widget _buildErrorImage(Attachment attachment) {
- return Center(
- child: Container(
- width: 200,
- height: 140,
- color: Color(0xffd0021B).withOpacity(.1),
- child: Center(
- child: Icon(
- Icons.error_outline,
- color: Colors.white,
- ),
- ),
- ),
- );
- }
-
- Widget _buildVideo(
- Attachment attachment,
- ) {
- VideoPlayerController videoController;
- if (_videoControllers.containsKey(attachment.assetUrl)) {
- videoController = _videoControllers[attachment.assetUrl];
+ if (widget.onMessageActions != null) {
+ widget.onMessageActions(context, widget.message);
} else {
- videoController = VideoPlayerController.network(attachment.assetUrl);
- _videoControllers[attachment.assetUrl] = videoController;
+ _showMessageBottomSheet(context);
}
+ return;
+ }
- return FutureBuilder(
- future: videoController.value.initialized
- ? Future.value(true)
- : videoController.initialize(),
- builder: (_, snapshot) {
- if (snapshot.connectionState != ConnectionState.done) {
- return Container(
- height: 100,
- width: 100,
- child: Center(
- child: CircularProgressIndicator(),
- ),
- );
- }
- ChewieController chewieController;
- if (_chuwieControllers.containsKey(attachment.assetUrl)) {
- chewieController = _chuwieControllers[attachment.assetUrl];
- } else {
- chewieController = ChewieController(
- allowFullScreen: widget.showVideoFullScreen,
- videoPlayerController: videoController,
- autoInitialize: false,
- aspectRatio: videoController.value.aspectRatio,
- errorBuilder: (_, e) {
- if (attachment.thumbUrl != null) {
- return Stack(
- children: [
- Container(
- decoration: BoxDecoration(
- image: DecorationImage(
- fit: BoxFit.cover,
- image: CachedNetworkImageProvider(
- attachment.thumbUrl,
- ),
- ),
- ),
- ),
- if (attachment.titleLink != null)
- Material(
- color: Colors.transparent,
- child: InkWell(
- onTap: () => _launchURL(attachment.titleLink),
- ),
- ),
- ],
- );
+ Widget _buildReplyIndicator(double leftPadding) {
+ return Padding(
+ padding: EdgeInsets.only(
+ left: leftPadding,
+ ),
+ child: Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: ReplyIndicator(
+ message: widget.message,
+ reversed: widget.reverse,
+ messageTheme: widget.messageTheme,
+ onTap: widget.onThreadTap != null
+ ? () {
+ widget.onThreadTap(widget.message);
}
-
- return _buildErrorImage(attachment);
- });
- _chuwieControllers[attachment.assetUrl] = chewieController;
- }
- return Chewie(
- key: ValueKey(
- 'ATTACHMENT-${attachment.title}-${widget.message.id}'),
- controller: chewieController,
- );
- },
+ : null,
+ ),
+ ),
);
}
- Future _launchURL(String url) async {
- if (await canLaunch(url)) {
- await launch(url);
- } else {
- Scaffold.of(context).showSnackBar(
- SnackBar(
- content: Text('Cannot launch the url'),
+ Widget _buildSendingIndicator() {
+ return Transform.translate(
+ offset: Offset(
+ 0,
+ 4,
+ ),
+ child: Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: SendingIndicator(
+ message: widget.message,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildUserAvatar() => Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ child: Transform.translate(
+ offset: Offset(
+ 0, widget.messageTheme.avatarTheme.constraints.maxHeight / 2),
+ child: UserAvatar(
+ user: widget.message.user,
+ onTap: widget.onUserAvatarTap,
+ constraints: widget.messageTheme.avatarTheme.constraints,
+ ),
+ ),
+ ),
+ );
+
+ Widget getFailedMessageWidget(
+ BuildContext context, {
+ EdgeInsetsGeometry padding,
+ }) {
+ Widget failedWidget;
+ if (widget.message.status == MessageSendingStatus.FAILED) {
+ failedWidget = Text(
+ 'MESSAGE FAILED · CLICK TO TRY AGAIN',
+ style: widget.messageTheme.messageText.copyWith(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(.5)
+ : Colors.black.withOpacity(.5),
+ fontSize: 11,
),
);
}
+ if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
+ failedWidget = Text(
+ 'MESSAGE UPDATE FAILED · CLICK TO TRY AGAIN',
+ style: widget.messageTheme.messageText.copyWith(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(.5)
+ : Colors.black.withOpacity(.5),
+ fontSize: 11,
+ ),
+ );
+ }
+ if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
+ failedWidget = Text(
+ 'MESSAGE DELETE FAILED · CLICK TO TRY AGAIN',
+ style: widget.messageTheme.messageText.copyWith(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withOpacity(.5)
+ : Colors.black.withOpacity(.5),
+ fontSize: 11,
+ ),
+ );
+ }
+
+ if (failedWidget != null) {
+ return Padding(
+ padding: padding ?? EdgeInsets.zero,
+ child: failedWidget,
+ );
+ }
+
+ return SizedBox();
}
- @override
- void dispose() {
- _videoControllers.values.forEach((element) {
- element.dispose();
- });
- super.dispose();
- }
-
- Widget _buildTimestamp(Alignment alignment) {
- return Padding(
- padding: const EdgeInsets.only(top: 5.0),
- child: RichText(
- text: TextSpan(
- style: _messageTheme.createdAt,
- children: [
- if (!_isMyMessage && widget.showOtherMessageUsername)
- TextSpan(
- text: widget.message.user.name,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- if (widget.message.createdAt != null)
- TextSpan(
- text:
- Jiffy(widget.message.createdAt.toLocal()).format(' HH:mm'),
- ),
- ],
+ Widget _buildTextBubble(BuildContext context) {
+ return GestureDetector(
+ onTap: () => retryMessage(context),
+ onLongPress: () => onLongPress(context),
+ child: Material(
+ shape: widget.shape ??
+ ContinuousRectangleBorder(
+ side: widget.borderSide ??
+ BorderSide(
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white.withAlpha(24)
+ : Colors.black.withAlpha(24),
+ ),
+ borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
+ ),
+ color: _getBackgroundColor(),
+ child: Transform(
+ transform: Matrix4.rotationY(widget.reverse ? pi : 0),
+ alignment: Alignment.center,
+ child: Padding(
+ padding: widget.textPadding,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ getFailedMessageWidget(context),
+ _buildText(context),
+ ],
+ ),
+ ),
),
),
);
}
- BoxDecoration _buildBoxDecoration(bool rectBorders) {
- return BoxDecoration(
- border: _isMyMessage
- ? null
- : Border.all(
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white.withAlpha(24)
- : Colors.black.withAlpha(24)),
- borderRadius: BorderRadius.only(
- topLeft: Radius.circular((_isMyMessage || !rectBorders) ? 16 : 2),
- bottomLeft: Radius.circular(_isMyMessage ? 16 : 2),
- topRight: Radius.circular((_isMyMessage && rectBorders) ? 2 : 16),
- bottomRight: Radius.circular(_isMyMessage ? 2 : 16),
- ),
- color: widget.message.status == MessageSendingStatus.FAILED
- ? Color(0xffd0021B).withOpacity(.1)
- : _messageTheme.messageBackgroundColor,
- );
+ Color _getBackgroundColor() {
+ return (widget.message.status == MessageSendingStatus.FAILED ||
+ widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
+ widget.message.status == MessageSendingStatus.FAILED_DELETE)
+ ? Color(0xffd0021B).withOpacity(.1)
+ : widget.messageTheme.messageBackgroundColor;
}
- @override
- bool get wantKeepAlive {
- return widget.message.attachments.isNotEmpty;
+ void retryMessage(BuildContext context) {
+ final channel = StreamChannel.of(context).channel;
+ if (widget.message.status == MessageSendingStatus.FAILED) {
+ channel.sendMessage(widget.message);
+ return;
+ }
+ if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) {
+ StreamChat.of(context).client.updateMessage(
+ widget.message,
+ channel.cid,
+ );
+ return;
+ }
+
+ if (widget.message.status == MessageSendingStatus.FAILED_DELETE) {
+ StreamChat.of(context).client.deleteMessage(
+ widget.message,
+ channel.cid,
+ );
+ return;
+ }
+ }
+
+ Widget _buildText(BuildContext context) {
+ return widget.textBuilder != null
+ ? widget.textBuilder(context, widget.message)
+ : MessageText(
+ message: widget.message,
+ onMentionTap: widget.onMentionTap,
+ messageTheme: widget.messageTheme,
+ );
}
}
-class _ReactionBubblePainter extends CustomPainter {
+class ReactionBubblePainter extends CustomPainter {
final Color color;
- _ReactionBubblePainter(this.color);
+ ReactionBubblePainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color;
final path = Path();
- path.arcToPoint(Offset(-6, -6));
- path.arcToPoint(Offset(0, 10));
- path.arcToPoint(Offset(6, -6));
+ path.lineTo(-2, -6);
+ path.lineTo(0, 10);
+ path.lineTo(10, -6);
+ path.lineTo(-2, -6);
canvas.drawPath(path, paint);
}
diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart
index 221dd3d8..56127476 100644
--- a/lib/src/reaction_picker.dart
+++ b/lib/src/reaction_picker.dart
@@ -24,56 +24,62 @@ class ReactionPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
- mainAxisSize: MainAxisSize.min,
- children: reactionToEmoji.keys.map((reactionType) {
- final ownReactionIndex = message.ownReactions
- ?.indexWhere((reaction) => reaction.type == reactionType) ??
- -1;
- return Column(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- IconButton(
- iconSize: size,
- icon: Text(
- reactionToEmoji[reactionType],
- style: TextStyle(
- fontSize: size - 10,
+ return Container(
+ color: Colors.black87,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: reactionToEmoji.keys.map((reactionType) {
+ final ownReactionIndex = message.ownReactions
+ ?.indexWhere((reaction) => reaction.type == reactionType) ??
+ -1;
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ IconButton(
+ iconSize: size,
+ icon: Text(
+ reactionToEmoji[reactionType],
+ style: TextStyle(
+ fontSize: size - 10,
+ ),
),
+ onPressed: () {
+ if (ownReactionIndex != -1) {
+ removeReaction(
+ context, message.ownReactions[ownReactionIndex]);
+ } else {
+ sendReaction(context, reactionType);
+ }
+ },
),
- onPressed: () {
- if (ownReactionIndex != -1) {
- removeReaction(context, reactionType);
- } else {
- sendReaction(context, reactionType);
- }
- },
- ),
- ownReactionIndex != -1
- ? Padding(
- padding: const EdgeInsets.only(bottom: 4.0),
- child: Text(
- message.ownReactions[ownReactionIndex].score.toString(),
- style: TextStyle(color: Colors.white),
- ),
- )
- : SizedBox(),
- ],
- );
- }).toList(),
+ ownReactionIndex != -1
+ ? Padding(
+ padding: const EdgeInsets.only(bottom: 4.0),
+ child: Text(
+ message.ownReactions[ownReactionIndex].score.toString(),
+ style: TextStyle(color: Colors.white),
+ ),
+ )
+ : SizedBox(),
+ ],
+ );
+ }).toList(),
+ ),
);
}
+ /// Add a reaction to the message
void sendReaction(BuildContext context, String reactionType) {
- channel.sendReaction(message.id, reactionType);
+ channel.sendReaction(message, reactionType);
Navigator.of(context).pop();
}
- void removeReaction(BuildContext context, String reactionType) {
- channel.deleteReaction(message.id, reactionType);
+ /// Remove a reaction from the message
+ void removeReaction(BuildContext context, Reaction reaction) {
+ channel.deleteReaction(message, reaction);
Navigator.of(context).pop();
}
}
diff --git a/lib/src/reply_indicator.dart b/lib/src/reply_indicator.dart
new file mode 100644
index 00000000..cdb1cd57
--- /dev/null
+++ b/lib/src/reply_indicator.dart
@@ -0,0 +1,56 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+/// A reply button indicator
+class ReplyIndicator extends StatelessWidget {
+ final Message message;
+ final VoidCallback onTap;
+ final bool reversed;
+ final MessageTheme messageTheme;
+
+ const ReplyIndicator({
+ Key key,
+ this.message,
+ this.onTap,
+ this.reversed = false,
+ this.messageTheme,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ var row = [
+ Text(
+ 'Replies: ${message.replyCount}',
+ style: messageTheme?.replies,
+ ),
+ Transform(
+ transform: Matrix4.rotationY(reversed ? 0 : pi),
+ alignment: Alignment.center,
+ child: Icon(
+ Icons.subdirectory_arrow_left,
+ color: Theme.of(context).brightness == Brightness.dark
+ ? Colors.white12
+ : Colors.black12,
+ ),
+ ),
+ ];
+
+ if (!reversed) {
+ row = row.reversed.toList();
+ }
+
+ return GestureDetector(
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2.0),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: row,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/sending_indicator.dart b/lib/src/sending_indicator.dart
new file mode 100644
index 00000000..81e7dfef
--- /dev/null
+++ b/lib/src/sending_indicator.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+/// Used to show the sending status of the message
+class SendingIndicator extends StatelessWidget {
+ final Message message;
+
+ const SendingIndicator({
+ Key key,
+ this.message,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ if (message.status == MessageSendingStatus.SENT || message.status == null) {
+ return CircleAvatar(
+ radius: 4,
+ backgroundColor: StreamChatTheme.of(context).accentColor,
+ child: Icon(
+ Icons.done,
+ color: Colors.white,
+ size: 4,
+ ),
+ );
+ }
+ if (message.status == MessageSendingStatus.SENDING ||
+ message.status == MessageSendingStatus.UPDATING) {
+ return CircleAvatar(
+ radius: 4,
+ backgroundColor: Colors.grey,
+ child: Icon(
+ Icons.access_time,
+ size: 4,
+ color: Colors.white,
+ ),
+ );
+ }
+ if (message.status == MessageSendingStatus.FAILED ||
+ message.status == MessageSendingStatus.FAILED_UPDATE ||
+ message.status == MessageSendingStatus.FAILED_DELETE) {
+ return CircleAvatar(
+ radius: 4,
+ backgroundColor: Color(0xffd0021B).withOpacity(.1),
+ child: Icon(
+ Icons.error_outline,
+ size: 4,
+ color: Colors.white,
+ ),
+ );
+ }
+
+ return SizedBox();
+ }
+}
diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart
index 2943441e..62775281 100644
--- a/lib/src/stream_channel.dart
+++ b/lib/src/stream_channel.dart
@@ -50,8 +50,14 @@ class StreamChannelState extends State {
/// The stream notifying the state of queryMessage call
Stream get queryMessage => _queryMessageController.stream;
+ bool _paginationEnded = false;
+
/// Calls [channel.query] updating [queryMessage] stream
void queryMessages() {
+ if (_paginationEnded) {
+ return;
+ }
+
_queryMessageController.add(true);
String firstId;
@@ -67,6 +73,9 @@ class StreamChannelState extends State {
),
)
.then((res) {
+ if (res.messages.isEmpty) {
+ _paginationEnded = true;
+ }
_queryMessageController.add(false);
}).catchError((e, stack) {
_queryMessageController.addError(e, stack);
@@ -75,8 +84,11 @@ class StreamChannelState extends State {
/// Calls [channel.getReplies] updating [queryMessage] stream
Future getReplies(String parentId) async {
+ if (_paginationEnded) {
+ return;
+ }
+
_queryMessageController.add(true);
- print('PARENT $parentId');
String firstId;
if (widget.channel.state.threads.containsKey(parentId)) {
@@ -96,12 +108,16 @@ class StreamChannelState extends State {
),
)
.then((res) {
+ if (res.messages.isEmpty) {
+ _paginationEnded = true;
+ }
_queryMessageController.add(false);
}).catchError((e, stack) {
_queryMessageController.addError(e, stack);
});
}
+ /// Query the channel members and watchers
Future queryMembersAndWatchers() async {
await widget.channel.query(
membersPagination: PaginationParams(
@@ -133,14 +149,18 @@ class StreamChannelState extends State {
initialData: widget.channel.state != null,
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data) {
- return Scaffold(
- body: Center(
+ return Container(
+ height: 30,
+ child: Center(
child: CircularProgressIndicator(),
),
);
} else if (snapshot.hasError) {
- return Center(
- child: Text(snapshot.error),
+ return Container(
+ height: 30,
+ child: Center(
+ child: Text(snapshot.error),
+ ),
);
} else {
return widget.child;
diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart
index acd829ff..92394ff0 100644
--- a/lib/src/stream_chat.dart
+++ b/lib/src/stream_chat.dart
@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
@@ -59,47 +58,27 @@ class StreamChat extends StatefulWidget {
}
}
-class StreamChatState extends State
- with AutomaticKeepAliveClientMixin {
- final List _subscriptions = [];
+/// The current state of the StreamChat widget
+class StreamChatState extends State with WidgetsBindingObserver {
Client get client => widget.client;
- final GlobalKey _navigatorKey = GlobalKey();
+ Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
- super.build(context);
final theme = _getTheme(context, widget.streamChatThemeData);
return StreamChatTheme(
data: theme,
child: Builder(
builder: (context) {
final materialTheme = Theme.of(context);
- final streamChatTheme = StreamChatTheme.of(context);
+ final streamTheme = StreamChatTheme.of(context);
return Theme(
data: materialTheme.copyWith(
- accentColor: streamChatTheme.accentColor,
- scaffoldBackgroundColor: streamChatTheme.backgroundColor,
- ),
- child: WillPopScope(
- onWillPop: () async {
- if (_navigatorKey.currentState.canPop()) {
- _navigatorKey.currentState.pop();
- return false;
- } else {
- return true;
- }
- },
- child: Navigator(
- initialRoute: '/',
- key: _navigatorKey,
- onGenerateRoute: (settings) {
- return MaterialPageRoute(
- settings: settings,
- builder: (_) => widget.child,
- );
- },
- ),
+ primaryIconTheme: streamTheme.primaryIconTheme,
+ accentColor: streamTheme.accentColor,
+ scaffoldBackgroundColor: streamTheme.backgroundColor,
),
+ child: widget.child,
);
},
),
@@ -114,6 +93,7 @@ class StreamChatState extends State
final theme = defaultTheme.copyWith(
primaryColor: themeData?.primaryColor,
defaultChannelImage: themeData?.defaultChannelImage,
+ primaryIconTheme: themeData?.primaryIconTheme,
defaultUserImage: themeData?.defaultUserImage,
backgroundColor: themeData?.backgroundColor,
channelTheme: defaultTheme.channelTheme.copyWith(
@@ -174,86 +154,85 @@ class StreamChatState extends State
title: themeData?.channelPreviewTheme?.title,
lastMessageAt: themeData?.channelPreviewTheme?.lastMessageAt,
subtitle: themeData?.channelPreviewTheme?.subtitle,
+ unreadCounterColor: themeData?.channelPreviewTheme?.unreadCounterColor,
),
);
return theme;
}
- @override
- void initState() {
- super.initState();
- _subscriptions.add(widget.client.on('message.new').listen((Event e) {
- final index = channels.indexWhere((c) => c.cid == e.cid);
- if (index > 0) {
- final channel = channels.removeAt(index);
- channels.insert(0, channel);
- _channelsController.add(channels);
- }
- }));
- }
-
/// The current user
User get user => widget.client.state.user;
/// The current user as a stream
Stream get userStream => widget.client.state.userStream;
- /// The current channel list
- final List channels = [];
-
- /// The current channel list as a stream
- Stream> get channelsStream => _channelsController.stream;
-
- final BehaviorSubject> _channelsController = BehaviorSubject();
-
- final BehaviorSubject _queryChannelsLoadingController =
- BehaviorSubject.seeded(false);
-
- /// The stream notifying the state of queryChannel call
- Stream get queryChannelsLoading =>
- _queryChannelsLoadingController.stream;
-
- /// Calls [client.queryChannels] updating [queryChannelsLoading] stream
- Future queryChannels({
- Map filter,
- List sortOptions,
- PaginationParams paginationParams,
- Map options,
- }) async {
- if (_queryChannelsLoadingController.value) {
- return;
- }
- _queryChannelsLoadingController.sink.add(true);
-
- try {
- final res = await widget.client.queryChannels(
- filter: filter,
- sort: sortOptions,
- options: options,
- paginationParams: paginationParams,
- );
- channels.addAll(res);
- _channelsController.sink.add(channels);
- } catch (e) {
- _channelsController.sink.addError(e);
- } finally {
- _queryChannelsLoadingController.sink.add(false);
- }
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
}
- /// Clear the current channel list
- void clearChannels() {
- channels.clear();
+ StreamSubscription _newMessageSubscription;
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.paused) {
+ if (client.showLocalNotification != null) {
+ _newMessageSubscription = client
+ .on(EventType.messageNew)
+ .where((e) => e.user?.id != user.id)
+ .where((e) => e.message.silent != true)
+ .listen((event) async {
+ var channel = client.state.channels[event.cid];
+
+ if (channel == null) {
+ channel = client.channel(
+ event.type,
+ id: event.cid.split(':')[1],
+ );
+ await channel.query();
+ }
+
+ client.showLocalNotification(
+ event.message,
+ ChannelModel(
+ id: channel.id,
+ createdAt: channel.createdAt,
+ extraData: channel.extraData,
+ type: channel.type,
+ memberCount: channel.memberCount,
+ frozen: channel.frozen,
+ cid: channel.cid,
+ deletedAt: channel.deletedAt,
+ config: channel.config,
+ createdBy: channel.createdBy,
+ updatedAt: channel.updatedAt,
+ lastMessageAt: channel.lastMessageAt,
+ ),
+ );
+ });
+ _disconnectTimer = Timer(client.backgroundKeepAlive, () {
+ client.disconnect();
+ });
+ } else {
+ client.disconnect();
+ }
+ } else if (state == AppLifecycleState.resumed) {
+ _newMessageSubscription?.cancel();
+ if (_disconnectTimer?.isActive == true) {
+ _disconnectTimer.cancel();
+ } else {
+ if (client.wsConnectionStatus.value == ConnectionStatus.disconnected) {
+ NotificationService.handleIosMessageQueue(client);
+ client.connect();
+ }
+ }
+ }
}
@override
void dispose() {
- _subscriptions.forEach((s) => s.cancel());
- _queryChannelsLoadingController.close();
- _channelsController.close();
+ WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
-
- @override
- bool get wantKeepAlive => true;
}
diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart
index ca574716..e75d30c3 100644
--- a/lib/src/stream_chat_theme.dart
+++ b/lib/src/stream_chat_theme.dart
@@ -24,7 +24,16 @@ class StreamChatTheme extends InheritedWidget {
/// Use this method to get the current [StreamChatThemeData] instance
static StreamChatThemeData of(BuildContext context) {
- return context.dependOnInheritedWidgetOfExactType().data;
+ final streamChatTheme =
+ context.dependOnInheritedWidgetOfExactType();
+
+ if (streamChatTheme == null) {
+ throw Exception(
+ 'You must have a StreamChatTheme widget at the top of your widget tree',
+ );
+ }
+
+ return streamChatTheme.data;
}
}
@@ -60,6 +69,9 @@ class StreamChatThemeData {
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
+ /// Primary icon theme
+ final IconThemeData primaryIconTheme;
+
/// Create a theme from scratch
StreamChatThemeData({
this.primaryColor,
@@ -72,6 +84,7 @@ class StreamChatThemeData {
this.ownMessageTheme,
this.defaultChannelImage,
this.defaultUserImage,
+ this.primaryIconTheme,
});
/// Create a theme from a Material [Theme]
@@ -80,6 +93,7 @@ class StreamChatThemeData {
return defaultTheme.copyWith(
accentColor: theme.accentColor,
+ primaryIconTheme: theme.primaryIconTheme,
primaryColor: theme.colorScheme.primary,
secondaryColor: theme.colorScheme.secondary,
backgroundColor: theme.scaffoldBackgroundColor,
@@ -114,10 +128,12 @@ class StreamChatThemeData {
MessageTheme otherMessageTheme,
Widget Function(BuildContext, Channel) defaultChannelImage,
Widget Function(BuildContext, User) defaultUserImage,
+ IconThemeData primaryIconTheme,
}) =>
StreamChatThemeData(
primaryColor: primaryColor ?? this.primaryColor,
secondaryColor: secondaryColor ?? this.secondaryColor,
+ primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
accentColor: accentColor ?? this.accentColor,
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
@@ -190,6 +206,8 @@ class StreamChatThemeData {
return StreamChatThemeData(
accentColor: accentColor,
primaryColor: isDark ? Colors.black : Colors.white,
+ primaryIconTheme:
+ IconThemeData(color: isDark ? Colors.white : Colors.black),
defaultChannelImage: (context, channel) => SizedBox(),
backgroundColor: isDark ? Colors.black : Colors.white,
defaultUserImage: (context, user) => Center(
@@ -199,6 +217,7 @@ class StreamChatThemeData {
),
),
channelPreviewTheme: ChannelPreviewTheme(
+ unreadCounterColor: Color(0xffd0021B),
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
@@ -423,12 +442,14 @@ class ChannelPreviewTheme {
final TextStyle subtitle;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
+ final Color unreadCounterColor;
const ChannelPreviewTheme({
this.title,
this.subtitle,
this.lastMessageAt,
this.avatarTheme,
+ this.unreadCounterColor,
});
ChannelPreviewTheme copyWith({
@@ -436,12 +457,14 @@ class ChannelPreviewTheme {
TextStyle subtitle,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
+ Color unreadCounterColor,
}) =>
ChannelPreviewTheme(
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
+ unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
);
}
diff --git a/lib/src/system_message.dart b/lib/src/system_message.dart
new file mode 100644
index 00000000..7454b0ee
--- /dev/null
+++ b/lib/src/system_message.dart
@@ -0,0 +1,100 @@
+import 'package:flutter/material.dart';
+import 'package:jiffy/jiffy.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+
+/// It shows a date divider depending on the date difference
+class SystemMessage extends StatelessWidget {
+ final Message message;
+
+ const SystemMessage({
+ Key key,
+ @required this.message,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final divider = Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Divider(),
+ ),
+ );
+
+ final createdAt = Jiffy(message.createdAt.toLocal());
+ final now = DateTime.now();
+ final hourInfo = createdAt.format('h:mm a');
+
+ String dayInfo;
+ if (Jiffy(createdAt).isSame(now, Units.DAY)) {
+ dayInfo = 'TODAY';
+ } else if (Jiffy(createdAt)
+ .isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
+ dayInfo = 'YESTERDAY';
+ } else if (Jiffy(createdAt).isAfter(
+ now.subtract(Duration(days: 7)),
+ Units.DAY,
+ )) {
+ dayInfo = createdAt.format('EEEE').toUpperCase();
+ } else if (Jiffy(createdAt).isAfter(
+ Jiffy(now).subtract(years: 1),
+ Units.DAY,
+ )) {
+ dayInfo = createdAt.format('dd/MM').toUpperCase();
+ } else {
+ dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
+ }
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ divider,
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Text(
+ message.text,
+ style: TextStyle(
+ fontSize: 10,
+ color: Theme.of(context)
+ .textTheme
+ .headline6
+ .color
+ .withOpacity(.5),
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ Text.rich(
+ TextSpan(
+ children: [
+ TextSpan(
+ text: dayInfo,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ TextSpan(text: ' AT'),
+ TextSpan(text: ' $hourInfo'),
+ ],
+ style: TextStyle(
+ fontWeight: FontWeight.normal,
+ ),
+ ),
+ style: TextStyle(
+ fontSize: 10,
+ color: Theme.of(context)
+ .textTheme
+ .headline6
+ .color
+ .withOpacity(.5),
+ ),
+ ),
+ ],
+ ),
+ ),
+ divider,
+ ],
+ );
+ }
+}
diff --git a/lib/src/thread_header.dart b/lib/src/thread_header.dart
index 07f10e60..ebff1b02 100644
--- a/lib/src/thread_header.dart
+++ b/lib/src/thread_header.dart
@@ -62,6 +62,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// The message parent of this thread
final Message parent;
+ /// Instantiate a new ThreadHeader
ThreadHeader({
Key key,
@required this.parent,
@@ -129,9 +130,6 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
child: Icon(
Icons.close,
size: 15,
- color: Theme.of(context).brightness == Brightness.dark
- ? Colors.white
- : Colors.black,
),
),
),
diff --git a/lib/src/typing_indicator.dart b/lib/src/typing_indicator.dart
index a759ecde..57427da6 100644
--- a/lib/src/typing_indicator.dart
+++ b/lib/src/typing_indicator.dart
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/stream_channel.dart';
/// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget {
+ /// Instantiate a new TypingIndicator
const TypingIndicator({
Key key,
this.channel,
diff --git a/lib/src/unread_indicator.dart b/lib/src/unread_indicator.dart
new file mode 100644
index 00000000..73f57d05
--- /dev/null
+++ b/lib/src/unread_indicator.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
+
+class UnreadIndicator extends StatelessWidget {
+ const UnreadIndicator({
+ Key key,
+ @required this.channel,
+ }) : super(key: key);
+
+ final Channel channel;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: CircleAvatar(
+ backgroundColor:
+ StreamChatTheme.of(context).channelPreviewTheme.unreadCounterColor,
+ radius: 6,
+ child: Text(
+ '${channel.state.unreadCount}',
+ style: TextStyle(fontSize: 8),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 00000000..c427099b
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,14 @@
+import 'package:flutter/material.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+Future launchURL(BuildContext context, String url) async {
+ if (await canLaunch(url)) {
+ await launch(url);
+ } else {
+ Scaffold.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Cannot launch the url'),
+ ),
+ );
+ }
+}
diff --git a/lib/src/video_attachment.dart b/lib/src/video_attachment.dart
new file mode 100644
index 00000000..a903147b
--- /dev/null
+++ b/lib/src/video_attachment.dart
@@ -0,0 +1,155 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:chewie/chewie.dart';
+import 'package:flutter/material.dart';
+import 'package:stream_chat_flutter/src/full_screen_video.dart';
+import 'package:stream_chat_flutter/src/utils.dart';
+import 'package:stream_chat_flutter/stream_chat_flutter.dart';
+import 'package:video_player/video_player.dart';
+
+import 'attachment_error.dart';
+import 'attachment_title.dart';
+
+class VideoAttachment extends StatefulWidget {
+ final Attachment attachment;
+ final MessageTheme messageTheme;
+ final Size size;
+
+ VideoAttachment({
+ Key key,
+ @required this.attachment,
+ @required this.messageTheme,
+ this.size,
+ }) : super(key: key);
+
+ @override
+ _VideoAttachmentState createState() => _VideoAttachmentState();
+}
+
+class _VideoAttachmentState extends State {
+ ChewieController _chewieController;
+ VideoPlayerController _videoPlayerController;
+ bool initialized = false;
+
+ @override
+ Widget build(BuildContext context) {
+ if (!initialized) {
+ return Container(
+ height: widget.size?.height ?? 100,
+ width: widget.size?.width ?? 100,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }
+ _chewieController = ChewieController(
+ videoPlayerController: _videoPlayerController,
+ autoInitialize: true,
+ showControls: false,
+ aspectRatio: _videoPlayerController.value.aspectRatio,
+ errorBuilder: (_, e) {
+ if (widget.attachment.thumbUrl != null) {
+ return Stack(
+ children: [
+ Container(
+ height: widget.size?.height,
+ width: widget.size?.width,
+ decoration: BoxDecoration(
+ image: DecorationImage(
+ fit: BoxFit.cover,
+ image: CachedNetworkImageProvider(
+ widget.attachment.thumbUrl,
+ ),
+ ),
+ ),
+ ),
+ if (widget.attachment.titleLink != null)
+ Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () =>
+ launchURL(context, widget.attachment.titleLink),
+ ),
+ ),
+ ],
+ );
+ }
+ return AttachmentError(
+ attachment: widget.attachment,
+ size: widget.size,
+ );
+ });
+
+ return GestureDetector(
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => FullScreenVideo(
+ attachment: widget.attachment,
+ ),
+ ),
+ );
+ },
+ child: Container(
+ height: widget.size?.height,
+ width: widget.size?.width,
+ child: Flex(
+ direction: Axis.vertical,
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Expanded(
+ child: FittedBox(
+ fit: BoxFit.cover,
+ child: Stack(
+ children: [
+ Chewie(
+ controller: _chewieController,
+ ),
+ Positioned.fill(
+ child: Center(
+ child: Material(
+ shape: CircleBorder(),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Icon(Icons.play_arrow),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ if (widget.attachment.title != null)
+ Material(
+ child: AttachmentTitle(
+ messageTheme: widget.messageTheme,
+ attachment: widget.attachment,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _videoPlayerController =
+ VideoPlayerController.network(widget.attachment.assetUrl);
+ _videoPlayerController.initialize().whenComplete(() {
+ setState(() {
+ initialized = true;
+ });
+ });
+ }
+
+ @override
+ void dispose() {
+ _videoPlayerController?.dispose();
+ _chewieController?.dispose();
+ super.dispose();
+ }
+}
diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart
index 857336ce..3000a503 100644
--- a/lib/stream_chat_flutter.dart
+++ b/lib/stream_chat_flutter.dart
@@ -5,12 +5,25 @@ export 'src/channel_image.dart';
export 'src/channel_list_view.dart';
export 'src/channel_name.dart';
export 'src/channel_preview.dart';
+export 'src/channels_bloc.dart';
+export 'src/date_divider.dart';
+export 'src/deleted_message.dart';
+export 'src/file_attachment.dart';
+export 'src/full_screen_video.dart';
+export 'src/giphy_attachment.dart';
+export 'src/image_attachment.dart';
export 'src/message_input.dart';
export 'src/message_list_view.dart';
+export 'src/message_text.dart';
export 'src/message_widget.dart';
export 'src/reaction_picker.dart';
+export 'src/reply_indicator.dart';
+export 'src/sending_indicator.dart';
export 'src/stream_channel.dart';
export 'src/stream_chat.dart';
export 'src/stream_chat_theme.dart';
+export 'src/system_message.dart';
export 'src/thread_header.dart';
export 'src/typing_indicator.dart';
+export 'src/user_avatar.dart';
+export 'src/video_attachment.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index e81a8654..77840411 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
-version: 0.1.35
+version: 0.2.1-alpha+11
environment:
sdk: ">=2.3.0 <3.0.0"
@@ -9,19 +9,21 @@ environment:
dependencies:
flutter:
sdk: flutter
- rxdart: ^0.24.0
+ photo_view: ^0.9.2
+ rxdart: ^0.24.1
jiffy: ^3.0.1
- cached_network_image: ^2.1.0+1
- flutter_markdown: ^0.3.5
- url_launcher: ^5.4.2
- video_player: ^0.10.8+1
+ flutter_portal: ^0.1.0
+ cached_network_image: ^2.2.0+1
+ flutter_markdown: ^0.4.2
+ url_launcher: ^5.4.11
+ video_player: ^0.10.11+1
chewie: ^0.9.10
- file_picker: ^1.6.3+2
- image_picker: ^0.6.5
- flutter_keyboard_visibility: ^2.0.0
- stream_chat: ^0.1.28
+ file_picker: ^1.12.0
+ image_picker: ^0.6.7+2
+ flutter_keyboard_visibility: ^3.2.1
+ stream_chat: ^0.2.0-alpha+23
mime: ^0.9.6+3
- visibility_detector: ^0.1.4
+ visibility_detector: ^0.1.5
http_parser: ^3.1.4
dev_dependencies: