Merge pull request #25 from GetStream/feature/notifications
Offline storage and notifications
This commit is contained in:
+104
-29
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
/// 
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+662
-998
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user