Merge pull request #25 from GetStream/feature/notifications

Offline storage and notifications
This commit is contained in:
Salvatore Giordano
2020-07-06 15:07:15 +02:00
committed by GitHub
59 changed files with 4048 additions and 1719 deletions
+104 -29
View File
@@ -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
+10 -3
View File
@@ -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
+2 -2
View File
@@ -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
+3
View File
@@ -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'
@@ -7,7 +7,7 @@
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name="io.flutter.app.FlutterApplication"
android:name=".Application"
android:label="example"
android:icon="@mipmap/ic_launcher">
<activity
@@ -17,6 +17,10 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -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"))
}
}
+1
View File
@@ -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'
}
}
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Notifications</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>
@@ -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 ?? "<NoContent>")"
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<Member>()
/// An extra data for the channel.
public let extraData: Codable?
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.stream.flutter</string>
</array>
</dict>
</plist>
+2 -2
View File
@@ -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
+231 -7
View File
@@ -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
+215 -17
View File
@@ -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 = "<group>"; };
0BC14C51242B5A7A0028DE94 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0BC14C5A242B5ED90028DE94 /* Notifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notifications.entitlements; sourceTree = "<group>"; };
0BC14C5B242B5FF50028DE94 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -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 = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -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 = "<group>";
};
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 = "<group>";
@@ -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 = (
+35 -7
View File
@@ -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")
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.stream.flutter</string>
</array>
</dict>
</plist>
+25 -19
View File
@@ -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<Message> 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;
+27 -26
View File
@@ -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(),
),
);
}
+21 -20
View File
@@ -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(
+133
View File
@@ -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: <Widget>[
Expanded(
child: MessageListView(
messageBuilder: _messageBuilder,
),
),
MessageInput(),
],
),
);
}
Widget _messageBuilder(
BuildContext context,
MessageDetails details,
List<Message> 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,
);
}
}
+85 -15
View File
@@ -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<String, dynamic> 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(),
),
);
}
+19 -18
View File
@@ -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(),
),
);
}
+3 -3
View File
@@ -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');
+20 -17
View File
@@ -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(),
),
);
}
+2
View File
@@ -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:
+53
View File
@@ -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(),
);
}
}
+37
View File
@@ -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,
),
),
),
);
}
}
+56
View File
@@ -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: <Widget>[
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,
),
],
),
),
);
}
}
-3
View File
@@ -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,
),
),
);
+1
View File
@@ -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,
+70 -18
View File
@@ -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<ChannelListView> {
class _ChannelListViewState extends State<ChannelListView>
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<ChannelListView> {
);
},
child: StreamBuilder<List<Channel>>(
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<ChannelListView> {
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<ChannelListView> {
),
FlatButton(
onPressed: () {
streamChat.queryChannels(
channelsProvider.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
@@ -215,7 +223,7 @@ class _ChannelListViewState extends State<ChannelListView> {
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<ChannelListView> {
);
} else {
child = ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: widget.onImageTap != null
? () {
@@ -283,15 +292,27 @@ class _ChannelListViewState extends State<ChannelListView> {
),
);
} else {
return _buildQueryProgressIndicator(context, streamChat);
return _buildQueryProgressIndicator(context, channelsProvider);
}
}
Widget _buildQueryProgressIndicator(context, StreamChatState streamChat) {
Widget _buildQueryProgressIndicator(
context, ChannelsBlocState channelsProvider) {
return StreamBuilder<bool>(
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<ChannelListView> {
);
}
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<ChannelListView> {
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<ChannelListView> {
);
_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();
}
}
+1
View File
@@ -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,
+18 -15
View File
@@ -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: <Widget>[
_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') {
+144
View File
@@ -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<ChannelsBlocState>();
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<ChannelsBloc>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
/// The current channel list
List<Channel> get channels => _channelsController.value;
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController =
BehaviorSubject.seeded([]);
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
_queryChannelsLoadingController.stream;
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> 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<Channel>.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<StreamSubscription> _subscriptions = [];
@override
void initState() {
super.initState();
final client = StreamChat.of(context).client;
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.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;
}
+79
View File
@@ -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: <Widget>[
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,
],
);
}
}
+27
View File
@@ -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,
),
),
);
}
}
+32
View File
@@ -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),
),
),
),
);
}
}
+29
View File
@@ -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,
),
),
);
}
}
+78
View File
@@ -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<FullScreenVideo> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
final GlobalKey<ScaffoldState> _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();
}
}
+93
View File
@@ -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: <Widget>[
Stack(
children: <Widget>[
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,
),
],
);
}
}
+99
View File
@@ -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: <Widget>[
Column(
children: <Widget>[
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,
),
),
),
),
],
),
);
}
}
+209
View File
@@ -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<String, String> 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: <Widget>[
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: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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);
}
},
);
}
}
+273 -176
View File
@@ -17,6 +17,21 @@ import '../stream_chat_flutter.dart';
import 'stream_channel.dart';
typedef FileUploader = Future<String> Function(File, Channel);
typedef AttachmentThumbnailBuilder = Widget Function(
BuildContext,
_SendingAttachment,
);
enum ActionsLocation {
left,
right,
}
enum DefaultAttachmentTypes {
image,
video,
file,
}
/// Inactive state
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png)
@@ -61,9 +76,11 @@ typedef FileUploader = Future<String> 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<Message> 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<Widget> actions;
/// The location of the custom actions
final ActionsLocation actionsLocation;
/// Map that defines a thumbnail builder for an attachment type
final Map<String, AttachmentThumbnailBuilder> 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<MessageInputState>();
if (messageInputState == null) {
throw Exception(
'You must have a MessageInput widget as anchestor of your widget tree');
}
return messageInputState;
}
}
class _MessageInputState extends State<MessageInput> {
class MessageInputState extends State<MessageInput> {
final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode();
final List<User> _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<MessageInput> {
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
_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<MessageInput> {
}
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<MessageInput> {
}
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<List<Member>> 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<MessageInput> {
],
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<List<Member>>(
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<MessageInput> {
}
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<MessageInput> {
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<MessageInput> {
);
}
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<MessageInput> {
color: Colors.transparent,
child: IconButton(
onPressed: () {
_showAttachmentModal();
showAttachmentModal();
},
icon: Icon(
Icons.add_circle_outline,
@@ -544,7 +619,8 @@ class _MessageInputState extends State<MessageInput> {
);
}
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
});
}
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<MessageInput> {
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<MessageInput> {
Future<String> _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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<Attachment> _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<MessageInput> {
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<MessageInput> {
}
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<MessageInput> {
}
});
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<MessageInput> {
}
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,
});
}
+216 -170
View File
@@ -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<Message>,
);
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<Message> 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;
}
}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png)
///
@@ -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<MessageListView> {
},
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<MessageListView> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessageWidget(
key: ValueKey<String>(
'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<MessageListView> {
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<String>('MESSAGE-${message.id}'),
builder: (_) => widget.messageBuilder(context, message, i),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
_messages,
i,
),
_messages),
);
} else {
messageWidget = MessageWidget(
key: ValueKey<String>('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: <Widget>[
messageWidget,
Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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<MessageListView> {
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<MessageListView> {
}
Widget _buildTopMessage(
Message message,
Message nextMessage,
StreamChannelState streamChannelState,
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (_) => widget.messageBuilder(context, message, 0),
key: ValueKey<String>('TOP-MESSAGE'),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
_messages,
_messages.length - 1,
),
_messages,
),
);
} else {
messageWidget = MessageWidget(
key: ValueKey<String>('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<MessageListView> {
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<MessageListView> {
}
Widget _buildBottomMessage(
StreamChannelState streamChannel,
Message previousMessage,
Message message,
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (_) => widget.messageBuilder(context, message, 0),
key: ValueKey<String>('BOTTOM-MESSAGE'),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
_messages,
0,
),
_messages,
),
);
} else {
messageWidget = MessageWidget(
key: ValueKey<String>('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<String>('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<MessageListView> {
);
}
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<Message> 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<MessageListView> {
Stream<List<Message>> 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<MessageListView> {
.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 ||
+64
View File
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
+47 -41
View File
@@ -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: <Widget>[
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: <Widget>[
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();
}
}
+56
View File
@@ -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,
),
),
);
}
}
+54
View File
@@ -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();
}
}
+25 -5
View File
@@ -50,8 +50,14 @@ class StreamChannelState extends State<StreamChannel> {
/// The stream notifying the state of queryMessage call
Stream<bool> 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<StreamChannel> {
),
)
.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<StreamChannel> {
/// Calls [channel.getReplies] updating [queryMessage] stream
Future<void> 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<StreamChannel> {
),
)
.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<void> queryMembersAndWatchers() async {
await widget.channel.query(
membersPagination: PaginationParams(
@@ -133,14 +149,18 @@ class StreamChannelState extends State<StreamChannel> {
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;
+71 -92
View File
@@ -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<StreamChat>
with AutomaticKeepAliveClientMixin {
final List<StreamSubscription> _subscriptions = [];
/// The current state of the StreamChat widget
class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
Client get client => widget.client;
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
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<StreamChat>
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<StreamChat>
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<User> get userStream => widget.client.state.userStream;
/// The current channel list
final List<Channel> channels = [];
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
_queryChannelsLoadingController.stream;
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> 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;
}
+24 -1
View File
@@ -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<StreamChatTheme>().data;
final streamChatTheme =
context.dependOnInheritedWidgetOfExactType<StreamChatTheme>();
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,
);
}
+100
View File
@@ -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: <Widget>[
divider,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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,
],
);
}
}
+1 -3
View File
@@ -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,
),
),
),
+1
View File
@@ -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,
+28
View File
@@ -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),
),
),
);
}
}
+14
View File
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> 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'),
),
);
}
}
+155
View File
@@ -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<VideoAttachment> {
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: <Widget>[
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: <Widget>[
Expanded(
child: FittedBox(
fit: BoxFit.cover,
child: Stack(
children: <Widget>[
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();
}
}
+13
View File
@@ -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';
+13 -11
View File
@@ -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: