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
|
- Fix overflow in mentions overlay
|
||||||
|
|
||||||
## 0.1.23
|
## 0.2.0-alpha+2
|
||||||
|
|
||||||
- Hotfix
|
- Add better mime detection
|
||||||
|
|
||||||
## 0.1.22
|
## 0.2.0-alpha+1
|
||||||
|
|
||||||
- Better mime type detection
|
|
||||||
|
|
||||||
## 0.1.21
|
|
||||||
|
|
||||||
- Fix video loading and error
|
- 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
|
- Add message configuration properties to MessageListView
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ The example is available under the [example](https://github.com/GetStream/stream
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
dependencies:
|
dependencies:
|
||||||
stream_chat_flutter: ^0.1.21
|
stream_chat_flutter: ^0.1.19
|
||||||
```
|
```
|
||||||
|
|
||||||
You should then run `flutter packages get`
|
You should then run `flutter packages get`
|
||||||
|
|
||||||
### Alpha version
|
### 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.
|
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
|
### 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)
|
- [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)
|
- [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
|
### UI Components
|
||||||
|
|
||||||
@@ -157,6 +158,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
|
## Contributing
|
||||||
|
|
||||||
We welcome code changes that improve this library or fix a problem,
|
We welcome code changes that improve this library or fix a problem,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ include: package:pedantic/analysis_options.yaml
|
|||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
- lib/**/*.g.dart
|
- lib/**/*.g.dart
|
||||||
- example/*
|
- example/**
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
@@ -44,7 +44,7 @@ linter:
|
|||||||
- package_prefixed_library_names
|
- package_prefixed_library_names
|
||||||
- prefer_is_not_empty
|
- prefer_is_not_empty
|
||||||
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
||||||
# - public_member_api_docs
|
- public_member_api_docs
|
||||||
- slash_for_doc_comments
|
- slash_for_doc_comments
|
||||||
# - sort_constructors_first
|
# - sort_constructors_first
|
||||||
# - sort_unnamed_constructors_first
|
# - sort_unnamed_constructors_first
|
||||||
|
|||||||
@@ -64,4 +64,7 @@ dependencies {
|
|||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.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. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
android:name=".Application"
|
||||||
android:label="example"
|
android:label="example"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@@ -17,6 +17,10 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<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 {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
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
|
# 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.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
@@ -63,7 +63,7 @@ target 'Runner' do
|
|||||||
|
|
||||||
# Keep pod path relative so it can be checked into Podfile.lock.
|
# Keep pod path relative so it can be checked into Podfile.lock.
|
||||||
pod 'Flutter', :path => 'Flutter'
|
pod 'Flutter', :path => 'Flutter'
|
||||||
|
pod 'StreamChatClient'
|
||||||
# Plugin Pods
|
# Plugin Pods
|
||||||
|
|
||||||
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
|
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
|
||||||
|
|||||||
+231
-7
@@ -1,23 +1,168 @@
|
|||||||
PODS:
|
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):
|
- file_picker (0.0.1):
|
||||||
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- 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 (1.0.0)
|
||||||
|
- flutter_apns (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_keyboard_visibility (0.7.0):
|
- flutter_keyboard_visibility (0.7.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_local_notifications (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_plugin_android_lifecycle (0.0.1):
|
- flutter_plugin_android_lifecycle (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 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):
|
- image_picker (0.0.1):
|
||||||
- Flutter
|
- 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):
|
- path_provider (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_macos (0.0.1):
|
- path_provider_macos (0.0.1):
|
||||||
- Flutter
|
- 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):
|
- sqflite (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (~> 2.7.2)
|
- 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):
|
- url_launcher (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
@@ -33,13 +178,21 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
|
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_apns (from `.symlinks/plugins/flutter_apns/ios`)
|
||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/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`)
|
- flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`)
|
||||||
- image_picker (from `.symlinks/plugins/image_picker/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 (from `.symlinks/plugins/path_provider/ios`)
|
||||||
- path_provider_macos (from `.symlinks/plugins/path_provider_macos/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`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
|
- StreamChatClient
|
||||||
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
|
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
|
||||||
- url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`)
|
- url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`)
|
||||||
- url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`)
|
- url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`)
|
||||||
@@ -49,23 +202,62 @@ DEPENDENCIES:
|
|||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
|
- DKImagePickerController
|
||||||
|
- DKPhotoGallery
|
||||||
|
- Firebase
|
||||||
|
- FirebaseAnalytics
|
||||||
|
- FirebaseAnalyticsInterop
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreDiagnostics
|
||||||
|
- FirebaseCoreDiagnosticsInterop
|
||||||
|
- FirebaseInstallations
|
||||||
|
- FirebaseInstanceID
|
||||||
|
- FirebaseMessaging
|
||||||
|
- FLAnimatedImage
|
||||||
- FMDB
|
- FMDB
|
||||||
|
- GoogleAppMeasurement
|
||||||
|
- GoogleDataTransport
|
||||||
|
- GoogleDataTransportCCTSupport
|
||||||
|
- GoogleUtilities
|
||||||
|
- GzipSwift
|
||||||
|
- nanopb
|
||||||
|
- PromisesObjC
|
||||||
|
- Protobuf
|
||||||
|
- ReachabilitySwift
|
||||||
|
- SDWebImage
|
||||||
|
- SDWebImageFLPlugin
|
||||||
|
- Starscream
|
||||||
|
- StreamChatClient
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
|
firebase_messaging:
|
||||||
|
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_apns:
|
||||||
|
:path: ".symlinks/plugins/flutter_apns/ios"
|
||||||
flutter_keyboard_visibility:
|
flutter_keyboard_visibility:
|
||||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||||
|
flutter_local_notifications:
|
||||||
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
|
:path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios"
|
||||||
image_picker:
|
image_picker:
|
||||||
:path: ".symlinks/plugins/image_picker/ios"
|
:path: ".symlinks/plugins/image_picker/ios"
|
||||||
|
moor_ffi:
|
||||||
|
:path: ".symlinks/plugins/moor_ffi/ios"
|
||||||
path_provider:
|
path_provider:
|
||||||
:path: ".symlinks/plugins/path_provider/ios"
|
:path: ".symlinks/plugins/path_provider/ios"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
:path: ".symlinks/plugins/path_provider_macos/ios"
|
: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:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
@@ -82,22 +274,54 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/wakelock/ios"
|
:path: ".symlinks/plugins/wakelock/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
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: 0e3d915762c693b495b44d77113d4970485de6ec
|
||||||
|
flutter_apns: f516b118e423fe7c0a38771180549c4d6cb67c2f
|
||||||
flutter_keyboard_visibility: 6195387fb6d8f46e5cd6dda4a4154e41f800f545
|
flutter_keyboard_visibility: 6195387fb6d8f46e5cd6dda4a4154e41f800f545
|
||||||
flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b
|
flutter_local_notifications: 9e4738ce2471c5af910d961a6b7eadcf57c50186
|
||||||
|
flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f
|
GoogleAppMeasurement: c29d405ff76e18551b5d158eaba6753fda8c7542
|
||||||
path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
|
GoogleDataTransport: a857c6a002d201b524dd4bc2ed7e7355ed07e785
|
||||||
|
GoogleDataTransportCCTSupport: 32f75fbe904c82772fcbb6b6bd4525bfb6f2a862
|
||||||
|
GoogleUtilities: ad0f3b691c67909d03a3327cc205222ab8f42e0e
|
||||||
|
GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
|
||||||
|
image_picker: 66aa71bc96850a90590a35d4c4a2907b0d823109
|
||||||
|
moor_ffi: d66c9470c18e9cb333423bbcb493c105c6c774c6
|
||||||
|
nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd
|
||||||
|
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
|
||||||
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
|
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
|
sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0
|
||||||
url_launcher: a1c0cc845906122c4784c542523d8cacbded5626
|
Starscream: 4bb2f9942274833f7b4d296a55504dcfc7edb7b0
|
||||||
|
StreamChatClient: 91b0f585e7dc92ade58e657daffafb16d485b2a6
|
||||||
|
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
|
||||||
url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313
|
url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313
|
||||||
url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c
|
url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c
|
||||||
video_player: 69c5f029fac4ffe4fc8a85ea7f7b793709661549
|
video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
|
||||||
video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7
|
video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7
|
||||||
wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4
|
wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4
|
||||||
|
|
||||||
PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83
|
PODFILE CHECKSUM: 5cc7e2f1316491ee530029e2e8391f4100d41fbd
|
||||||
|
|
||||||
COCOAPODS: 1.8.4
|
COCOAPODS: 1.8.4
|
||||||
|
|||||||
@@ -7,28 +7,45 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
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 */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
7DEC2743BD66C91B700A3B97 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BB2E5E4E236267EDF0D8817 /* Pods_Runner.framework */; };
|
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 */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
/* End PBXBuildFile section */
|
/* 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 */
|
/* 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 */ = {
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
|
|
||||||
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
|
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -36,11 +53,15 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@@ -58,12 +78,17 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
0BC14C4A242B5A7A0028DE94 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
|
|
||||||
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
|
|
||||||
7DEC2743BD66C91B700A3B97 /* Pods_Runner.framework in Frameworks */,
|
7DEC2743BD66C91B700A3B97 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -71,12 +96,20 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
0BC14C4E242B5A7A0028DE94 /* Notifications */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0BC14C5A242B5ED90028DE94 /* Notifications.entitlements */,
|
||||||
|
0BC14C4F242B5A7A0028DE94 /* NotificationService.swift */,
|
||||||
|
0BC14C51242B5A7A0028DE94 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = Notifications;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
3B80C3931E831B6300D905FE /* App.framework */,
|
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEBA1CF902C7004384FC /* Flutter.framework */,
|
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
@@ -87,8 +120,9 @@
|
|||||||
97C146E51CF9000F007C117D = {
|
97C146E51CF9000F007C117D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
9740EEB11CF90186004384FC /* Flutter */,
|
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
0BC14C4E242B5A7A0028DE94 /* Notifications */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
CF168B61BAB91958681C7C21 /* Pods */,
|
CF168B61BAB91958681C7C21 /* Pods */,
|
||||||
BC09A38346C8B2CD72199469 /* Frameworks */,
|
BC09A38346C8B2CD72199469 /* Frameworks */,
|
||||||
@@ -99,6 +133,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
0BC14C4D242B5A7A0028DE94 /* Notifications.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -106,6 +141,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0BC14C5B242B5FF50028DE94 /* Runner.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
@@ -147,6 +183,23 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget 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 */ = {
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
@@ -159,10 +212,12 @@
|
|||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
5702861DACEDB848A3E454E8 /* [CP] Embed Pods Frameworks */,
|
5702861DACEDB848A3E454E8 /* [CP] Embed Pods Frameworks */,
|
||||||
|
0BC14C55242B5A7A0028DE94 /* Embed App Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
0BC14C53242B5A7A0028DE94 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
@@ -175,13 +230,20 @@
|
|||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 1140;
|
||||||
LastUpgradeCheck = 1020;
|
LastUpgradeCheck = 1020;
|
||||||
ORGANIZATIONNAME = "The Chromium Authors";
|
ORGANIZATIONNAME = "The Chromium Authors";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
0BC14C4C242B5A7A0028DE94 = {
|
||||||
|
CreatedOnToolsVersion = 11.4;
|
||||||
|
DevelopmentTeam = EHV7XZLAHA;
|
||||||
|
ProvisioningStyle = Manual;
|
||||||
|
};
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
CreatedOnToolsVersion = 7.3.1;
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
DevelopmentTeam = EHV7XZLAHA;
|
DevelopmentTeam = EHV7XZLAHA;
|
||||||
LastSwiftMigration = 1100;
|
LastSwiftMigration = 1100;
|
||||||
|
ProvisioningStyle = Manual;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -199,11 +261,19 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
97C146ED1CF9000F007C117D /* Runner */,
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
0BC14C4C242B5A7A0028DE94 /* Notifications */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
0BC14C4B242B5A7A0028DE94 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -230,7 +300,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
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 */ = {
|
5702861DACEDB848A3E454E8 /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
@@ -286,6 +356,14 @@
|
|||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
0BC14C49242B5A7A0028DE94 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
0BC14C50242B5A7A0028DE94 /* NotificationService.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -297,6 +375,14 @@
|
|||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
0BC14C53242B5A7A0028DE94 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 0BC14C4C242B5A7A0028DE94 /* Notifications */;
|
||||||
|
targetProxy = 0BC14C52242B5A7A0028DE94 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
@@ -317,6 +403,90 @@
|
|||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
@@ -372,8 +542,12 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
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)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -382,13 +556,15 @@
|
|||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.stream.flutter;
|
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "flutter app example";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
@@ -507,8 +683,12 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
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)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -517,13 +697,15 @@
|
|||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.stream.flutter;
|
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "flutter app example";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -535,8 +717,12 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
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)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -545,13 +731,15 @@
|
|||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = io.stream.flutter;
|
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "flutter app example";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
@@ -561,6 +749,16 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList 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" */ = {
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -3,11 +3,39 @@ import Flutter
|
|||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
let sharedDefaults = UserDefaults(suiteName: "group.io.stream.flutter")
|
||||||
_ application: UIApplication,
|
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
override func application(
|
||||||
) -> Bool {
|
_ application: UIApplication,
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
) -> 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].
|
/// or to retrieve outer scope needed such as messages from the [Channel.state].
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
'b67pax5b2wdq',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
User(id: 'falling-mountain-7'),
|
User(id: 'super-band-9'),
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
@@ -36,12 +36,11 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Container(
|
builder: (context, child) => StreamChat(
|
||||||
child: StreamChat(
|
child: child,
|
||||||
client: client,
|
client: client,
|
||||||
child: ChannelListPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
home: ChannelListPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,17 +49,19 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ChannelListView(
|
body: ChannelsBloc(
|
||||||
filter: {
|
child: ChannelListView(
|
||||||
'members': {
|
filter: {
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
'members': {
|
||||||
}
|
'\$in': [StreamChat.of(context).user.id],
|
||||||
},
|
}
|
||||||
sort: [SortOption('last_message_at')],
|
},
|
||||||
pagination: PaginationParams(
|
sort: [SortOption('last_message_at')],
|
||||||
limit: 20,
|
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 isCurrentUser = StreamChat.of(context).user.id == message.user.id;
|
||||||
final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
|
final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
|
||||||
final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
|
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].
|
/// You can perform these more granular style changes using [StreamChatTheme.copyWith].
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
'b67pax5b2wdq',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
User(id: 'falling-mountain-7'),
|
User(id: 'super-band-9'),
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
@@ -45,23 +45,22 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
home: Container(
|
builder: (context, child) => StreamChat(
|
||||||
child: StreamChat(
|
child: child,
|
||||||
streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
|
client: client,
|
||||||
ownMessageTheme: MessageTheme(
|
streamChatThemeData: StreamChatThemeData.fromTheme(theme).copyWith(
|
||||||
messageBackgroundColor: Colors.black,
|
ownMessageTheme: MessageTheme(
|
||||||
messageText: TextStyle(
|
messageBackgroundColor: Colors.black,
|
||||||
color: Colors.white,
|
messageText: TextStyle(
|
||||||
),
|
color: Colors.white,
|
||||||
avatarTheme: AvatarTheme(
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
avatarTheme: AvatarTheme(
|
||||||
),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
client: client,
|
|
||||||
child: ChannelListPage(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
home: ChannelListPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,17 +69,19 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ChannelListView(
|
body: ChannelsBloc(
|
||||||
filter: {
|
child: ChannelListView(
|
||||||
'members': {
|
filter: {
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
'members': {
|
||||||
}
|
'\$in': [StreamChat.of(context).user.id],
|
||||||
},
|
}
|
||||||
sort: [SortOption('last_message_at')],
|
},
|
||||||
pagination: PaginationParams(
|
sort: [SortOption('last_message_at')],
|
||||||
limit: 20,
|
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]
|
/// - We retrieve the count of unread messages from [Channel.state]
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
'b67pax5b2wdq',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
User(id: 'falling-mountain-7'),
|
User(id: 'super-band-9'),
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
@@ -41,12 +41,11 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Container(
|
builder: (context, child) => StreamChat(
|
||||||
child: StreamChat(
|
child: child,
|
||||||
client: client,
|
client: client,
|
||||||
child: ChannelListPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
home: ChannelListPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,29 +54,31 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ChannelListView(
|
body: ChannelsBloc(
|
||||||
filter: {
|
child: ChannelListView(
|
||||||
'members': {
|
filter: {
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
'members': {
|
||||||
|
'\$in': [StreamChat.of(context).user.id],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
channelPreviewBuilder: _channelPreviewBuilder,
|
||||||
channelPreviewBuilder: _channelPreviewBuilder,
|
sort: [SortOption('last_message_at')],
|
||||||
sort: [SortOption('last_message_at')],
|
pagination: PaginationParams(
|
||||||
pagination: PaginationParams(
|
limit: 20,
|
||||||
limit: 20,
|
),
|
||||||
|
channelWidget: ChannelPage(),
|
||||||
),
|
),
|
||||||
channelWidget: ChannelPage(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _channelPreviewBuilder(BuildContext context, Channel channel) {
|
Widget _channelPreviewBuilder(BuildContext context, Channel channel) {
|
||||||
final lastMessage = channel.state.messages.reversed.firstWhere(
|
final lastMessage = channel.state.messages.reversed.firstWhere(
|
||||||
(message) => message.type != "deleted",
|
(message) => message.type != 'deleted',
|
||||||
orElse: () => null,
|
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;
|
final opacity = channel.state.unreadCount > .0 ? 1.0 : 0.5;
|
||||||
|
|
||||||
return ListTile(
|
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/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';
|
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 {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
|
showLocalNotification: Platform.isAndroid ? showLocalNotification : null,
|
||||||
|
persistenceEnabled: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
@@ -12,6 +77,8 @@ void main() async {
|
|||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_initNotifications(client);
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +93,13 @@ class MyApp extends StatelessWidget {
|
|||||||
theme: ThemeData.light(),
|
theme: ThemeData.light(),
|
||||||
darkTheme: ThemeData.dark(),
|
darkTheme: ThemeData.dark(),
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
home: Container(
|
builder: (context, widget) {
|
||||||
child: StreamChat(
|
return StreamChat(
|
||||||
|
child: widget,
|
||||||
client: client,
|
client: client,
|
||||||
child: ChannelListPage(),
|
);
|
||||||
),
|
},
|
||||||
),
|
home: ChannelListPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,17 +108,19 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ChannelListView(
|
body: ChannelsBloc(
|
||||||
filter: {
|
child: ChannelListView(
|
||||||
'members': {
|
filter: {
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
'members': {
|
||||||
}
|
'\$in': [StreamChat.of(context).user.id],
|
||||||
},
|
}
|
||||||
sort: [SortOption('last_message_at')],
|
},
|
||||||
pagination: PaginationParams(
|
sort: [SortOption('last_message_at')],
|
||||||
limit: 20,
|
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.
|
/// [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 {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
'b67pax5b2wdq',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
User(id: 'falling-mountain-7'),
|
User(id: 'super-band-9'),
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
@@ -40,12 +40,11 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Container(
|
builder: (context, child) => StreamChat(
|
||||||
child: StreamChat(
|
client: client,
|
||||||
client: client,
|
child: child,
|
||||||
child: ChannelListPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
home: ChannelListPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,17 +53,19 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ChannelListView(
|
body: ChannelsBloc(
|
||||||
filter: {
|
child: ChannelListView(
|
||||||
'members': {
|
filter: {
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
'members': {
|
||||||
}
|
'\$in': [StreamChat.of(context).user.id],
|
||||||
},
|
}
|
||||||
sort: [SortOption('last_message_at')],
|
},
|
||||||
pagination: PaginationParams(
|
sort: [SortOption('last_message_at')],
|
||||||
limit: 20,
|
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.
|
/// If you now run the simulator you will see a single channel UI.
|
||||||
void main() async {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
'b67pax5b2wdq',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
User(id: 'falling-mountain-7'),
|
User(id: 'super-band-9'),
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
final channel = client.channel('messaging', id: 'godevs');
|
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].
|
/// 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 {
|
void main() async {
|
||||||
final client = Client(
|
final client = Client(
|
||||||
'b67pax5b2wdq',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.setUser(
|
await client.setUser(
|
||||||
User(id: 'falling-mountain-7'),
|
User(id: 'super-band-9'),
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(MyApp(client));
|
runApp(MyApp(client));
|
||||||
@@ -31,11 +31,12 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
builder: (context, child) => StreamChat(
|
||||||
|
child: child,
|
||||||
|
client: client,
|
||||||
|
),
|
||||||
home: Container(
|
home: Container(
|
||||||
child: StreamChat(
|
child: ChannelListPage(),
|
||||||
client: client,
|
|
||||||
child: ChannelListPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -45,17 +46,19 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ChannelListView(
|
body: ChannelsBloc(
|
||||||
filter: {
|
child: ChannelListView(
|
||||||
'members': {
|
filter: {
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
'members': {
|
||||||
}
|
'\$in': [StreamChat.of(context).user.id],
|
||||||
},
|
}
|
||||||
sort: [SortOption('last_message_at')],
|
},
|
||||||
pagination: PaginationParams(
|
sort: [SortOption('last_message_at')],
|
||||||
limit: 20,
|
pagination: PaginationParams(
|
||||||
|
limit: 20,
|
||||||
|
),
|
||||||
|
channelWidget: ChannelPage(),
|
||||||
),
|
),
|
||||||
channelWidget: ChannelPage(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter:
|
stream_chat_flutter:
|
||||||
path: ../
|
path: ../
|
||||||
|
flutter_apns: ^1.1.0
|
||||||
|
flutter_local_notifications: ^1.4.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
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(
|
child: Icon(
|
||||||
Icons.arrow_back_ios,
|
Icons.arrow_back_ios,
|
||||||
size: 15,
|
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].
|
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelImage extends StatelessWidget {
|
class ChannelImage extends StatelessWidget {
|
||||||
|
/// Instantiate a new ChannelImage
|
||||||
const ChannelImage({
|
const ChannelImage({
|
||||||
Key key,
|
Key key,
|
||||||
this.channel,
|
this.channel,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/channels_bloc.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'channel_preview.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].
|
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class ChannelListView extends StatefulWidget {
|
class ChannelListView extends StatefulWidget {
|
||||||
|
/// Instantiate a new ChannelListView
|
||||||
ChannelListView({
|
ChannelListView({
|
||||||
Key key,
|
Key key,
|
||||||
this.filter,
|
this.filter,
|
||||||
@@ -53,12 +55,14 @@ class ChannelListView extends StatefulWidget {
|
|||||||
this.sort,
|
this.sort,
|
||||||
this.pagination,
|
this.pagination,
|
||||||
this.onChannelTap,
|
this.onChannelTap,
|
||||||
|
this.onChannelLongPress,
|
||||||
this.channelWidget,
|
this.channelWidget,
|
||||||
this.channelPreviewBuilder,
|
this.channelPreviewBuilder,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.onImageTap,
|
this.onImageTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The builder that will be used in case of error
|
||||||
final Widget Function(Error error) errorBuilder;
|
final Widget Function(Error error) errorBuilder;
|
||||||
|
|
||||||
/// The query filters to use.
|
/// The query filters to use.
|
||||||
@@ -89,6 +93,9 @@ class ChannelListView extends StatefulWidget {
|
|||||||
/// with the widget [channelWidget] as child.
|
/// with the widget [channelWidget] as child.
|
||||||
final ChannelTapCallback onChannelTap;
|
final ChannelTapCallback onChannelTap;
|
||||||
|
|
||||||
|
/// Function called when long pressing on a channel
|
||||||
|
final Function(Channel) onChannelLongPress;
|
||||||
|
|
||||||
/// Widget used when opening a channel
|
/// Widget used when opening a channel
|
||||||
final Widget channelWidget;
|
final Widget channelWidget;
|
||||||
|
|
||||||
@@ -102,16 +109,17 @@ class ChannelListView extends StatefulWidget {
|
|||||||
_ChannelListViewState createState() => _ChannelListViewState();
|
_ChannelListViewState createState() => _ChannelListViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelListViewState extends State<ChannelListView> {
|
class _ChannelListViewState extends State<ChannelListView>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streamChat = StreamChat.of(context);
|
final channelsProvider = ChannelsBloc.of(context);
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
streamChat.clearChannels();
|
return channelsProvider.queryChannels(
|
||||||
return streamChat.queryChannels(
|
|
||||||
filter: widget.filter,
|
filter: widget.filter,
|
||||||
sortOptions: widget.sort,
|
sortOptions: widget.sort,
|
||||||
paginationParams: widget.pagination,
|
paginationParams: widget.pagination,
|
||||||
@@ -119,7 +127,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: StreamBuilder<List<Channel>>(
|
child: StreamBuilder<List<Channel>>(
|
||||||
stream: streamChat.channelsStream,
|
stream: channelsProvider.channelsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
if (snapshot.error is Error) {
|
if (snapshot.error is Error) {
|
||||||
@@ -157,7 +165,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
TextSpan(text: 'Error loading channels'),
|
TextSpan(text: 'Error loading channels'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
style: Theme.of(context).textTheme.title,
|
style: Theme.of(context).textTheme.headline6,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
@@ -167,7 +175,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
),
|
),
|
||||||
FlatButton(
|
FlatButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
streamChat.queryChannels(
|
channelsProvider.queryChannels(
|
||||||
filter: widget.filter,
|
filter: widget.filter,
|
||||||
sortOptions: widget.sort,
|
sortOptions: widget.sort,
|
||||||
paginationParams: widget.pagination,
|
paginationParams: widget.pagination,
|
||||||
@@ -215,7 +223,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
|
|
||||||
i = i ~/ 2;
|
i = i ~/ 2;
|
||||||
|
|
||||||
final streamChat = StreamChat.of(context);
|
final channelsProvider = ChannelsBloc.of(context);
|
||||||
if (i < channels.length) {
|
if (i < channels.length) {
|
||||||
final channel = channels[i];
|
final channel = channels[i];
|
||||||
|
|
||||||
@@ -267,6 +275,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = ChannelPreview(
|
child = ChannelPreview(
|
||||||
|
onLongPress: widget.onChannelLongPress,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
onImageTap: widget.onImageTap != null
|
onImageTap: widget.onImageTap != null
|
||||||
? () {
|
? () {
|
||||||
@@ -283,15 +292,27 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return _buildQueryProgressIndicator(context, streamChat);
|
return _buildQueryProgressIndicator(context, channelsProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQueryProgressIndicator(context, StreamChatState streamChat) {
|
Widget _buildQueryProgressIndicator(
|
||||||
|
context, ChannelsBlocState channelsProvider) {
|
||||||
return StreamBuilder<bool>(
|
return StreamBuilder<bool>(
|
||||||
stream: streamChat.queryChannelsLoading,
|
stream: channelsProvider.queryChannelsLoading,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
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(
|
return Container(
|
||||||
height: 100,
|
height: 100,
|
||||||
padding: EdgeInsets.all(32),
|
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 ==
|
if (_scrollController.position.maxScrollExtent ==
|
||||||
_scrollController.offset) {
|
_scrollController.offset &&
|
||||||
streamChat.queryChannels(
|
_scrollController.offset != 0) {
|
||||||
|
channelsProvider.queryChannels(
|
||||||
filter: widget.filter,
|
filter: widget.filter,
|
||||||
sortOptions: widget.sort,
|
sortOptions: widget.sort,
|
||||||
paginationParams: widget.pagination.copyWith(
|
paginationParams: widget.pagination.copyWith(
|
||||||
offset: streamChat.channels.length,
|
offset: channelsProvider.channels.length,
|
||||||
),
|
),
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
);
|
);
|
||||||
@@ -330,8 +352,11 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
final streamChat = StreamChat.of(context);
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
streamChat.queryChannels(
|
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
channelsBloc.queryChannels(
|
||||||
filter: widget.filter,
|
filter: widget.filter,
|
||||||
sortOptions: widget.sort,
|
sortOptions: widget.sort,
|
||||||
paginationParams: widget.pagination,
|
paginationParams: widget.pagination,
|
||||||
@@ -339,7 +364,34 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_scrollController.addListener(() {
|
_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.
|
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
||||||
class ChannelName extends StatelessWidget {
|
class ChannelName extends StatelessWidget {
|
||||||
|
/// Instantiate a new ChannelName
|
||||||
const ChannelName({
|
const ChannelName({
|
||||||
Key key,
|
Key key,
|
||||||
this.channel,
|
this.channel,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/unread_indicator.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'channel_name.dart';
|
import 'channel_name.dart';
|
||||||
@@ -22,6 +23,9 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
/// Function called when tapping this widget
|
/// Function called when tapping this widget
|
||||||
final void Function(Channel) onTap;
|
final void Function(Channel) onTap;
|
||||||
|
|
||||||
|
/// Function called when long pressing this widget
|
||||||
|
final void Function(Channel) onLongPress;
|
||||||
|
|
||||||
/// Channel displayed
|
/// Channel displayed
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
@required this.channel,
|
@required this.channel,
|
||||||
Key key,
|
Key key,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
this.onImageTap,
|
this.onImageTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -39,7 +44,14 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onTap(channel);
|
if (onTap != null) {
|
||||||
|
onTap(channel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
if (onLongPress != null) {
|
||||||
|
onLongPress(channel);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
leading: ChannelImage(
|
leading: ChannelImage(
|
||||||
onTap: onImageTap,
|
onTap: onImageTap,
|
||||||
@@ -53,16 +65,8 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildDate(context),
|
_buildDate(context),
|
||||||
if (channel.state.unreadCount > 0)
|
if (channel.state.unreadCount > 0)
|
||||||
Padding(
|
UnreadIndicator(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
channel: channel,
|
||||||
child: CircleAvatar(
|
|
||||||
backgroundColor: Color(0xffd0021B),
|
|
||||||
radius: 6,
|
|
||||||
child: Text(
|
|
||||||
'${channel.state.unreadCount}',
|
|
||||||
style: TextStyle(fontSize: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -118,16 +122,15 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
stream: channel.state.messagesStream,
|
stream: channel.state.messagesStream,
|
||||||
initialData: channel.state.messages,
|
initialData: channel.state.messages,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final messages = snapshot.data;
|
final lastMessage = channel.state.lastMessage;
|
||||||
final lastMessage = messages.isNotEmpty ? messages.last : null;
|
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
return SizedBox();
|
return SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
String text;
|
var text = lastMessage.text;
|
||||||
if (lastMessage.isDeleted) {
|
if (lastMessage.isDeleted) {
|
||||||
text = 'This message was deleted.';
|
text = 'This message was deleted.';
|
||||||
} else {
|
} else if (lastMessage.attachments != null) {
|
||||||
final prefix = lastMessage.attachments
|
final prefix = lastMessage.attachments
|
||||||
.map((e) {
|
.map((e) {
|
||||||
if (e.type == 'image') {
|
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';
|
import 'stream_channel.dart';
|
||||||
|
|
||||||
typedef FileUploader = Future<String> Function(File, Channel);
|
typedef FileUploader = Future<String> Function(File, Channel);
|
||||||
|
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||||
|
BuildContext,
|
||||||
|
_SendingAttachment,
|
||||||
|
);
|
||||||
|
|
||||||
|
enum ActionsLocation {
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DefaultAttachmentTypes {
|
||||||
|
image,
|
||||||
|
video,
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
|
||||||
/// Inactive state
|
/// 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].
|
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class MessageInput extends StatefulWidget {
|
class MessageInput extends StatefulWidget {
|
||||||
|
/// Instantiate a new MessageInput
|
||||||
MessageInput({
|
MessageInput({
|
||||||
Key key,
|
Key key,
|
||||||
this.onMessageSent,
|
this.onMessageSent,
|
||||||
|
this.preMessageSending,
|
||||||
this.parentMessage,
|
this.parentMessage,
|
||||||
this.editMessage,
|
this.editMessage,
|
||||||
this.maxHeight = 150,
|
this.maxHeight = 150,
|
||||||
@@ -72,6 +89,10 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.doImageUploadRequest,
|
this.doImageUploadRequest,
|
||||||
this.doFileUploadRequest,
|
this.doFileUploadRequest,
|
||||||
this.initialMessage,
|
this.initialMessage,
|
||||||
|
this.textEditingController,
|
||||||
|
this.actions,
|
||||||
|
this.actionsLocation = ActionsLocation.left,
|
||||||
|
this.attachmentThumbnailBuilders,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to edit
|
/// Message to edit
|
||||||
@@ -83,6 +104,10 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Function called after sending the message
|
/// Function called after sending the message
|
||||||
final void Function(Message) onMessageSent;
|
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
|
/// Parent message in case of a thread
|
||||||
final Message parentMessage;
|
final Message parentMessage;
|
||||||
|
|
||||||
@@ -101,33 +126,49 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Override file upload request
|
/// Override file upload request
|
||||||
final FileUploader doFileUploadRequest;
|
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
|
@override
|
||||||
_MessageInputState createState() => _MessageInputState(
|
MessageInputState createState() => MessageInputState();
|
||||||
doFileUploadRequest: doFileUploadRequest,
|
|
||||||
doImageUploadRequest: doImageUploadRequest,
|
/// 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 List<_SendingAttachment> _attachments = [];
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
final List<User> _mentionedUsers = [];
|
final List<User> _mentionedUsers = [];
|
||||||
FileUploader doImageUploadRequest;
|
|
||||||
FileUploader doFileUploadRequest;
|
|
||||||
|
|
||||||
TextEditingController _textController;
|
final _imagePicker = ImagePicker();
|
||||||
bool _inputEnabled = true;
|
bool _inputEnabled = true;
|
||||||
bool _messageIsPresent = false;
|
bool _messageIsPresent = false;
|
||||||
bool _typingStarted = false;
|
bool _typingStarted = false;
|
||||||
OverlayEntry _commandsOverlay, _mentionsOverlay;
|
OverlayEntry _commandsOverlay, _mentionsOverlay;
|
||||||
|
|
||||||
_MessageInputState({
|
/// The editing controller passed to the input TextField
|
||||||
this.doImageUploadRequest,
|
TextEditingController textEditingController;
|
||||||
this.doFileUploadRequest,
|
|
||||||
}) {
|
|
||||||
doImageUploadRequest ??= _uploadImage;
|
|
||||||
doFileUploadRequest ??= _uploadFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -164,8 +205,12 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
if (!widget.disableAttachments) _buildAttachmentButton(),
|
||||||
|
if (widget.actionsLocation == ActionsLocation.left)
|
||||||
|
...widget.actions ?? [],
|
||||||
_buildTextInput(context),
|
_buildTextInput(context),
|
||||||
_animateSendButton(context),
|
_animateSendButton(context),
|
||||||
|
if (widget.actionsLocation == ActionsLocation.right)
|
||||||
|
...widget.actions ?? [],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -193,10 +238,10 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
minLines: null,
|
minLines: null,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
_sendMessage(context);
|
sendMessage();
|
||||||
},
|
},
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
controller: _textController,
|
controller: textEditingController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
StreamChannel.of(context).channel.keyStroke();
|
StreamChannel.of(context).channel.keyStroke();
|
||||||
@@ -215,10 +260,10 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
Overlay.of(context).insert(_commandsOverlay);
|
Overlay.of(context).insert(_commandsOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_textController.selection.isCollapsed &&
|
if (textEditingController.selection.isCollapsed &&
|
||||||
(s[_textController.selection.start - 1] == '@' ||
|
(s[textEditingController.selection.start - 1] == '@' ||
|
||||||
_textController.text
|
textEditingController.text
|
||||||
.substring(0, _textController.selection.start)
|
.substring(0, textEditingController.selection.start)
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.last
|
.last
|
||||||
.contains('@'))) {
|
.contains('@'))) {
|
||||||
@@ -231,7 +276,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
_typingStarted = true;
|
_typingStarted = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
style: Theme.of(context).textTheme.body1,
|
style: Theme.of(context).textTheme.bodyText2,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Write a message',
|
hintText: 'Write a message',
|
||||||
@@ -278,7 +323,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OverlayEntry _buildCommandsOverlayEntry() {
|
OverlayEntry _buildCommandsOverlayEntry() {
|
||||||
final text = _textController.text;
|
final text = textEditingController.text;
|
||||||
final commands = StreamChannel.of(context)
|
final commands = StreamChannel.of(context)
|
||||||
.channel
|
.channel
|
||||||
.config
|
.config
|
||||||
@@ -343,11 +388,21 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OverlayEntry _buildMentionsOverlayEntry() {
|
OverlayEntry _buildMentionsOverlayEntry() {
|
||||||
final splits = _textController.text
|
final splits = textEditingController.text
|
||||||
.substring(0, _textController.value.selection.start)
|
.substring(0, textEditingController.value.selection.start)
|
||||||
.split('@');
|
.split('@');
|
||||||
final query = splits.last.toLowerCase();
|
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) {
|
final members = StreamChannel.of(context).channel.state.members?.where((m) {
|
||||||
return m.user.name.toLowerCase().contains(query);
|
return m.user.name.toLowerCase().contains(query);
|
||||||
})?.toList() ??
|
})?.toList() ??
|
||||||
@@ -375,36 +430,42 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
],
|
],
|
||||||
color: StreamChatTheme.of(context).primaryColor,
|
color: StreamChatTheme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: ListView(
|
child: FutureBuilder<List<Member>>(
|
||||||
padding: const EdgeInsets.all(0),
|
future: queryMembers ?? Future.value(members),
|
||||||
shrinkWrap: true,
|
initialData: members,
|
||||||
children: members
|
builder: (context, snapshot) {
|
||||||
.map((m) => ListTile(
|
return ListView(
|
||||||
leading: UserAvatar(
|
padding: const EdgeInsets.all(0),
|
||||||
user: m.user,
|
shrinkWrap: true,
|
||||||
),
|
children: snapshot.data
|
||||||
title: Text('${m.user.name}'),
|
.map((m) => ListTile(
|
||||||
onTap: () {
|
leading: UserAvatar(
|
||||||
_mentionedUsers.add(m.user);
|
user: m.user,
|
||||||
|
),
|
||||||
|
title: Text('${m.user.name}'),
|
||||||
|
onTap: () {
|
||||||
|
_mentionedUsers.add(m.user);
|
||||||
|
|
||||||
splits[splits.length - 1] = m.user.name;
|
splits[splits.length - 1] = m.user.name;
|
||||||
final rejoin = splits.join('@');
|
final rejoin = splits.join('@');
|
||||||
|
|
||||||
_textController.value = TextEditingValue(
|
textEditingController.value = TextEditingValue(
|
||||||
text: rejoin +
|
text: rejoin +
|
||||||
_textController.text
|
textEditingController.text.substring(
|
||||||
.substring(_textController.selection.start),
|
textEditingController
|
||||||
selection: TextSelection.collapsed(
|
.selection.start),
|
||||||
offset: rejoin.length,
|
selection: TextSelection.collapsed(
|
||||||
),
|
offset: rejoin.length,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
_mentionsOverlay?.remove();
|
_mentionsOverlay?.remove();
|
||||||
_mentionsOverlay = null;
|
_mentionsOverlay = null;
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -412,7 +473,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setCommand(Command c) {
|
void _setCommand(Command c) {
|
||||||
_textController.value = TextEditingValue(
|
textEditingController.value = TextEditingValue(
|
||||||
text: '/${c.name} ',
|
text: '/${c.name} ',
|
||||||
selection: TextSelection.collapsed(
|
selection: TextSelection.collapsed(
|
||||||
offset: c.name.length + 2,
|
offset: c.name.length + 2,
|
||||||
@@ -451,35 +512,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
width: 50,
|
width: 50,
|
||||||
child: _buildAttachment(attachment),
|
child: _buildAttachment(attachment),
|
||||||
),
|
),
|
||||||
Positioned(
|
_buildRemoveButton(attachment),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
attachment.uploaded
|
attachment.uploaded
|
||||||
? SizedBox()
|
? SizedBox()
|
||||||
: Positioned.fill(
|
: 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) {
|
Widget _buildAttachment(_SendingAttachment attachment) {
|
||||||
switch (attachment.type) {
|
if (widget.attachmentThumbnailBuilders
|
||||||
case FileType.image:
|
?.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
|
return attachment.file != null
|
||||||
? Image.file(
|
? Image.file(
|
||||||
attachment.file,
|
attachment.file,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: Image.network(
|
: Image.network(
|
||||||
attachment.url,
|
attachment.attachment.imageUrl ??
|
||||||
|
attachment.attachment.thumbUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FileType.video:
|
case 'video':
|
||||||
return Container(
|
return Container(
|
||||||
child: Icon(Icons.videocam),
|
child: Icon(Icons.videocam),
|
||||||
color: Colors.black26,
|
color: Colors.black26,
|
||||||
@@ -535,7 +610,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showAttachmentModal();
|
showAttachmentModal();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.add_circle_outline,
|
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) {
|
if (_focusNode.hasFocus) {
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
}
|
}
|
||||||
@@ -575,7 +651,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
leading: Icon(Icons.image),
|
leading: Icon(Icons.image),
|
||||||
title: Text('Upload a photo'),
|
title: Text('Upload a photo'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_pickFile(FileType.image, false);
|
pickFile(DefaultAttachmentTypes.image, false);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -583,7 +659,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
leading: Icon(Icons.video_library),
|
leading: Icon(Icons.video_library),
|
||||||
title: Text('Upload a video'),
|
title: Text('Upload a video'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_pickFile(FileType.video, false);
|
pickFile(DefaultAttachmentTypes.video, false);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -591,7 +667,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
leading: Icon(Icons.camera_alt),
|
leading: Icon(Icons.camera_alt),
|
||||||
title: Text('Photo from camera'),
|
title: Text('Photo from camera'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_pickFile(FileType.image, true);
|
pickFile(DefaultAttachmentTypes.image, true);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -599,7 +675,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
leading: Icon(Icons.videocam),
|
leading: Icon(Icons.videocam),
|
||||||
title: Text('Video from camera'),
|
title: Text('Video from camera'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_pickFile(FileType.video, true);
|
pickFile(DefaultAttachmentTypes.video, true);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -607,7 +683,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
leading: Icon(Icons.insert_drive_file),
|
leading: Icon(Icons.insert_drive_file),
|
||||||
title: Text('Upload a file'),
|
title: Text('Upload a file'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_pickFile(FileType.any, false);
|
pickFile(DefaultAttachmentTypes.file, false);
|
||||||
Navigator.pop(context);
|
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(() {
|
setState(() {
|
||||||
_inputEnabled = false;
|
_inputEnabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
File file;
|
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 (camera) {
|
||||||
if (type == FileType.image) {
|
PickedFile pickedFile;
|
||||||
file = await ImagePicker.pickImage(source: ImageSource.camera);
|
if (fileType == DefaultAttachmentTypes.image) {
|
||||||
} else if (type == FileType.video) {
|
pickedFile = await _imagePicker.getImage(source: ImageSource.camera);
|
||||||
file = await ImagePicker.pickVideo(source: ImageSource.camera);
|
} else if (fileType == DefaultAttachmentTypes.video) {
|
||||||
|
pickedFile = await _imagePicker.getVideo(source: ImageSource.camera);
|
||||||
}
|
}
|
||||||
|
file = File(pickedFile.path);
|
||||||
} else {
|
} 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);
|
file = await FilePicker.getFile(type: type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,16 +753,27 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
final attachment = _SendingAttachment(
|
final attachment = _SendingAttachment(
|
||||||
file: file,
|
file: file,
|
||||||
type: type,
|
attachment: Attachment(
|
||||||
|
localUri: file.uri,
|
||||||
|
type: attachmentType,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachments.add(attachment);
|
_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(() {
|
setState(() {
|
||||||
attachment.uploaded = true;
|
attachment.uploaded = true;
|
||||||
@@ -663,14 +782,22 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Future<String> _uploadAttachment(
|
Future<String> _uploadAttachment(
|
||||||
File file,
|
File file,
|
||||||
FileType type,
|
DefaultAttachmentTypes type,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
) async {
|
) async {
|
||||||
String url;
|
String url;
|
||||||
if (type == FileType.image) {
|
if (type == DefaultAttachmentTypes.image) {
|
||||||
url = await doImageUploadRequest(file, channel);
|
if (widget.doImageUploadRequest != null) {
|
||||||
|
url = await widget.doImageUploadRequest(file, channel);
|
||||||
|
} else {
|
||||||
|
url = await _uploadImage(file, channel);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
url = await doFileUploadRequest(file, channel);
|
if (widget.doFileUploadRequest != null) {
|
||||||
|
url = await widget.doFileUploadRequest(file, channel);
|
||||||
|
} else {
|
||||||
|
url = await _uploadFile(file, channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -714,25 +841,27 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
key: Key('sendButton'),
|
key: Key('sendButton'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_sendMessage(context);
|
sendMessage();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.send,
|
Icons.send,
|
||||||
|
color: StreamChatTheme.of(context).accentColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendMessage(BuildContext context) {
|
/// Sends the current message
|
||||||
final text = _textController.text.trim();
|
void sendMessage() async {
|
||||||
|
final text = textEditingController.text.trim();
|
||||||
if (text.isEmpty && _attachments.isEmpty) {
|
if (text.isEmpty && _attachments.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final attachments = List<_SendingAttachment>.from(_attachments);
|
final attachments = List<_SendingAttachment>.from(_attachments);
|
||||||
|
|
||||||
_textController.clear();
|
textEditingController.clear();
|
||||||
_attachments.clear();
|
_attachments.clear();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -751,21 +880,11 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
Message message;
|
Message message;
|
||||||
if (widget.editMessage != null) {
|
if (widget.editMessage != null) {
|
||||||
message = widget.editMessage.copyWith(
|
message = widget.editMessage.copyWith(
|
||||||
parentId: widget.parentMessage?.id,
|
|
||||||
text: text,
|
text: text,
|
||||||
attachments: widget.editMessage.attachments
|
attachments: _getAttachments(attachments).toList(),
|
||||||
.where((attachment) => attachment.type == 'giphy')
|
|
||||||
.toList() +
|
|
||||||
_getAttachments(attachments).toList(),
|
|
||||||
mentionedUsers:
|
mentionedUsers:
|
||||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
_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 {
|
} else {
|
||||||
message = (widget.initialMessage ?? Message()).copyWith(
|
message = (widget.initialMessage ?? Message()).copyWith(
|
||||||
parentId: widget.parentMessage?.id,
|
parentId: widget.parentMessage?.id,
|
||||||
@@ -774,34 +893,36 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
mentionedUsers:
|
mentionedUsers:
|
||||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
_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) {
|
if (widget.onMessageSent != null) {
|
||||||
widget.onMessageSent(message);
|
widget.onMessageSent(message);
|
||||||
|
} else {
|
||||||
|
if (widget.editMessage != null) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
|
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
|
||||||
return attachments.map((attachment) {
|
return attachments.map((attachment) {
|
||||||
String type;
|
return attachment.attachment;
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,12 +932,10 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
StreamChannel.of(context).queryMembersAndWatchers();
|
|
||||||
|
|
||||||
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
if (_commandsOverlay != null) {
|
if (_commandsOverlay != null) {
|
||||||
if (_textController.text.startsWith('/')) {
|
if (textEditingController.text.startsWith('/')) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||||
Overlay.of(context).insert(_commandsOverlay);
|
Overlay.of(context).insert(_commandsOverlay);
|
||||||
@@ -825,7 +944,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_mentionsOverlay != null) {
|
if (_mentionsOverlay != null) {
|
||||||
if (_textController.text.contains('@')) {
|
if (textEditingController.text.contains('@')) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_mentionsOverlay = _buildCommandsOverlayEntry();
|
_mentionsOverlay = _buildCommandsOverlayEntry();
|
||||||
Overlay.of(context).insert(_mentionsOverlay);
|
Overlay.of(context).insert(_mentionsOverlay);
|
||||||
@@ -842,44 +961,24 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (widget.editMessage != null) {
|
textEditingController =
|
||||||
_parseExistingMessage(widget.editMessage);
|
widget.textEditingController ?? TextEditingController();
|
||||||
} else if (widget.initialMessage != null) {
|
if (widget.editMessage != null || widget.initialMessage != null) {
|
||||||
_parseExistingMessage(widget.initialMessage);
|
_parseExistingMessage(widget.editMessage ?? widget.initialMessage);
|
||||||
} else {
|
|
||||||
_textController = TextEditingController();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _parseExistingMessage(Message message) {
|
void _parseExistingMessage(Message message) {
|
||||||
_textController = TextEditingController(text: message.text);
|
textEditingController.text = message.text;
|
||||||
|
|
||||||
_typingStarted = true;
|
_typingStarted = true;
|
||||||
_messageIsPresent = true;
|
_messageIsPresent = true;
|
||||||
|
|
||||||
message.attachments?.forEach((attachment) {
|
message.attachments?.forEach((attachment) {
|
||||||
if (attachment.type == 'image') {
|
_attachments.add(_SendingAttachment(
|
||||||
_attachments.add(_SendingAttachment(
|
attachment: attachment,
|
||||||
type: FileType.image,
|
uploaded: true,
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,15 +1002,13 @@ class _MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SendingAttachment {
|
class _SendingAttachment {
|
||||||
final File file;
|
File file;
|
||||||
final FileType type;
|
Attachment attachment;
|
||||||
String url;
|
|
||||||
bool uploaded;
|
bool uploaded;
|
||||||
|
|
||||||
_SendingAttachment({
|
_SendingAttachment({
|
||||||
this.url,
|
|
||||||
this.file,
|
this.file,
|
||||||
this.type,
|
this.attachment,
|
||||||
this.uploaded = false,
|
this.uploaded = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+216
-170
@@ -3,17 +3,56 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:stream_chat/stream_chat.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 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'message_widget.dart';
|
import 'date_divider.dart';
|
||||||
import 'stream_channel.dart';
|
import 'stream_channel.dart';
|
||||||
|
|
||||||
typedef MessageBuilder = Widget Function(BuildContext, Message, int index);
|
typedef MessageBuilder = Widget Function(
|
||||||
typedef ParentMessageBuilder = Widget Function(BuildContext, Message);
|
BuildContext,
|
||||||
|
MessageDetails,
|
||||||
|
List<Message>,
|
||||||
|
);
|
||||||
|
typedef ParentMessageBuilder = Widget Function(
|
||||||
|
BuildContext,
|
||||||
|
Message,
|
||||||
|
);
|
||||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
||||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
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].
|
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget appearance.
|
/// Modify it to change the widget appearance.
|
||||||
class MessageListView extends StatefulWidget {
|
class MessageListView extends StatefulWidget {
|
||||||
|
/// Instantiate a new MessageListView
|
||||||
MessageListView({
|
MessageListView({
|
||||||
Key key,
|
Key key,
|
||||||
this.messageBuilder,
|
this.messageBuilder,
|
||||||
@@ -62,11 +102,8 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.parentMessage,
|
this.parentMessage,
|
||||||
this.threadBuilder,
|
this.threadBuilder,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.showOtherMessageUsername = false,
|
this.dateDividerBuilder,
|
||||||
this.showVideoFullScreen = true,
|
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
|
||||||
this.onMentionTap,
|
|
||||||
this.onUserAvatarTap,
|
|
||||||
this.onMessageActions,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
@@ -85,20 +122,11 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Parent message in case of a thread
|
/// Parent message in case of a thread
|
||||||
final Message parentMessage;
|
final Message parentMessage;
|
||||||
|
|
||||||
/// If true show the other users username next to the timestamp of the message
|
/// Builder used to render date dividers
|
||||||
final bool showOtherMessageUsername;
|
final Widget Function(DateTime) dateDividerBuilder;
|
||||||
|
|
||||||
/// True if the video player will allow fullscreen mode
|
/// The ScrollPhysics used by the ListView
|
||||||
final bool showVideoFullScreen;
|
final ScrollPhysics scrollPhysics;
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
@@ -130,7 +158,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
},
|
},
|
||||||
child: ListView.custom(
|
child: ListView.custom(
|
||||||
key: Key('messageListView'),
|
key: Key('messageListView'),
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: widget.scrollPhysics,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
childrenDelegate: SliverChildBuilderDelegate(
|
childrenDelegate: SliverChildBuilderDelegate(
|
||||||
@@ -146,21 +174,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MessageWidget(
|
buildParentMessage(widget.parentMessage),
|
||||||
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,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -184,124 +198,58 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return _buildLoadingIndicator(streamChannel);
|
return _buildLoadingIndicator(streamChannel);
|
||||||
}
|
}
|
||||||
final message = _messages[i];
|
final message = _messages[i];
|
||||||
|
|
||||||
final previousMessage =
|
|
||||||
i < _messages.length - 1 ? _messages[i + 1] : null;
|
|
||||||
final nextMessage = i > 0 ? _messages[i - 1] : null;
|
final nextMessage = i > 0 ? _messages[i - 1] : null;
|
||||||
|
|
||||||
Widget messageWidget;
|
Widget messageWidget;
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
messageWidget = _buildBottomMessage(
|
messageWidget = _buildBottomMessage(
|
||||||
streamChannel,
|
|
||||||
previousMessage,
|
|
||||||
message,
|
|
||||||
context,
|
context,
|
||||||
|
message,
|
||||||
|
_messages,
|
||||||
|
streamChannel,
|
||||||
);
|
);
|
||||||
} else if (i == _messages.length - 1) {
|
} else if (i == _messages.length - 1) {
|
||||||
messageWidget = _buildTopMessage(
|
messageWidget = _buildTopMessage(
|
||||||
message,
|
|
||||||
nextMessage,
|
|
||||||
streamChannel,
|
|
||||||
context,
|
context,
|
||||||
|
message,
|
||||||
|
_messages,
|
||||||
|
streamChannel,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (widget.messageBuilder != null) {
|
if (widget.messageBuilder != null) {
|
||||||
messageWidget = Builder(
|
messageWidget = Builder(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
builder: (_) => widget.messageBuilder(context, message, i),
|
builder: (_) => widget.messageBuilder(
|
||||||
|
context,
|
||||||
|
MessageDetails(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
_messages,
|
||||||
|
i,
|
||||||
|
),
|
||||||
|
_messages),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageWidget = MessageWidget(
|
messageWidget = buildMessage(message, _messages, i);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextMessage != null &&
|
if (nextMessage != null &&
|
||||||
!Jiffy(message.createdAt.toLocal())
|
!Jiffy(message.createdAt.toLocal())
|
||||||
.isSame(nextMessage.createdAt.toLocal(), Units.DAY)) {
|
.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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
messageWidget,
|
messageWidget,
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
child: Row(
|
child: widget.dateDividerBuilder != null
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
? widget
|
||||||
children: <Widget>[
|
.dateDividerBuilder(nextMessage.createdAt.toLocal())
|
||||||
divider,
|
: DateDivider(
|
||||||
Padding(
|
dateTime: nextMessage.createdAt.toLocal(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
divider,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -329,9 +277,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
print((snapshot.error as Error).stackTrace.toString());
|
return Container(
|
||||||
return Center(
|
color: Color(0xffd0021B).withAlpha(26),
|
||||||
child: Text(snapshot.error.toString()),
|
child: Center(
|
||||||
|
child: Text('Error loading messages'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!snapshot.data) {
|
if (!snapshot.data) {
|
||||||
@@ -348,30 +298,28 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopMessage(
|
Widget _buildTopMessage(
|
||||||
Message message,
|
|
||||||
Message nextMessage,
|
|
||||||
StreamChannelState streamChannelState,
|
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
Message message,
|
||||||
|
List<Message> messages,
|
||||||
|
StreamChannelState streamChannel,
|
||||||
) {
|
) {
|
||||||
Widget messageWidget;
|
Widget messageWidget;
|
||||||
if (widget.messageBuilder != null) {
|
if (widget.messageBuilder != null) {
|
||||||
messageWidget = Builder(
|
messageWidget = Builder(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('TOP-MESSAGE'),
|
||||||
builder: (_) => widget.messageBuilder(context, message, 0),
|
builder: (_) => widget.messageBuilder(
|
||||||
|
context,
|
||||||
|
MessageDetails(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
_messages,
|
||||||
|
_messages.length - 1,
|
||||||
|
),
|
||||||
|
_messages,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageWidget = MessageWidget(
|
messageWidget = buildMessage(message, messages, _messages.length - 1);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
@@ -380,7 +328,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
onVisibilityChanged: (visibility) {
|
onVisibilityChanged: (visibility) {
|
||||||
final topIsVisible = visibility.visibleBounds != Rect.zero;
|
final topIsVisible = visibility.visibleBounds != Rect.zero;
|
||||||
if (topIsVisible && !_topWasVisible) {
|
if (topIsVisible && !_topWasVisible) {
|
||||||
streamChannelState.queryMessages();
|
streamChannel.queryMessages();
|
||||||
}
|
}
|
||||||
_topWasVisible = topIsVisible;
|
_topWasVisible = topIsVisible;
|
||||||
},
|
},
|
||||||
@@ -388,37 +336,35 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomMessage(
|
Widget _buildBottomMessage(
|
||||||
StreamChannelState streamChannel,
|
|
||||||
Message previousMessage,
|
|
||||||
Message message,
|
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
Message message,
|
||||||
|
List<Message> messages,
|
||||||
|
StreamChannelState streamChannel,
|
||||||
) {
|
) {
|
||||||
Widget messageWidget;
|
Widget messageWidget;
|
||||||
if (widget.messageBuilder != null) {
|
if (widget.messageBuilder != null) {
|
||||||
messageWidget = Builder(
|
messageWidget = Builder(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||||
builder: (_) => widget.messageBuilder(context, message, 0),
|
builder: (_) => widget.messageBuilder(
|
||||||
|
context,
|
||||||
|
MessageDetails(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
_messages,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
_messages,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
messageWidget = MessageWidget(
|
messageWidget = buildMessage(message, messages, 0);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||||
onVisibilityChanged: (visibility) {
|
onVisibilityChanged: (visibility) {
|
||||||
_isBottom = visibility.visibleBounds != Rect.zero;
|
_isBottom = visibility.visibleBounds != Rect.zero;
|
||||||
if (_isBottom && streamChannel.channel.config.readEvents) {
|
if (_isBottom && streamChannel.channel.config?.readEvents == true) {
|
||||||
if (streamChannel.channel.state.unreadCount > 0) {
|
if (streamChannel.channel.state.unreadCount > 0) {
|
||||||
streamChannel.channel.markRead();
|
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;
|
StreamSubscription _streamListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -442,11 +485,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Stream<List<Message>> stream;
|
Stream<List<Message>> stream;
|
||||||
|
|
||||||
if (widget.parentMessage == null) {
|
if (widget.parentMessage == null) {
|
||||||
stream = streamChannel.channel.state.messagesStream.map((messages) =>
|
stream = streamChannel.channel.state.messagesStream;
|
||||||
messages
|
|
||||||
.where((m) =>
|
|
||||||
!(m.status == MessageSendingStatus.FAILED && m.isDeleted))
|
|
||||||
.toList());
|
|
||||||
} else {
|
} else {
|
||||||
streamChannel.getReplies(widget.parentMessage.id);
|
streamChannel.getReplies(widget.parentMessage.id);
|
||||||
stream = streamChannel.channel.state.threadsStream
|
stream = streamChannel.channel.state.threadsStream
|
||||||
@@ -454,7 +493,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
.map((threads) => threads[widget.parentMessage.id]);
|
.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();
|
newMessages = newMessages.reversed.toList();
|
||||||
if (_messages.isEmpty ||
|
if (_messages.isEmpty ||
|
||||||
newMessages.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
color: Colors.black87,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: reactionToEmoji.keys.map((reactionType) {
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
final ownReactionIndex = message.ownReactions
|
mainAxisSize: MainAxisSize.min,
|
||||||
?.indexWhere((reaction) => reaction.type == reactionType) ??
|
children: reactionToEmoji.keys.map((reactionType) {
|
||||||
-1;
|
final ownReactionIndex = message.ownReactions
|
||||||
return Column(
|
?.indexWhere((reaction) => reaction.type == reactionType) ??
|
||||||
mainAxisSize: MainAxisSize.min,
|
-1;
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
return Column(
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
IconButton(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
iconSize: size,
|
children: <Widget>[
|
||||||
icon: Text(
|
IconButton(
|
||||||
reactionToEmoji[reactionType],
|
iconSize: size,
|
||||||
style: TextStyle(
|
icon: Text(
|
||||||
fontSize: size - 10,
|
reactionToEmoji[reactionType],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: size - 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (ownReactionIndex != -1) {
|
||||||
|
removeReaction(
|
||||||
|
context, message.ownReactions[ownReactionIndex]);
|
||||||
|
} else {
|
||||||
|
sendReaction(context, reactionType);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
ownReactionIndex != -1
|
||||||
if (ownReactionIndex != -1) {
|
? Padding(
|
||||||
removeReaction(context, reactionType);
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
} else {
|
child: Text(
|
||||||
sendReaction(context, reactionType);
|
message.ownReactions[ownReactionIndex].score.toString(),
|
||||||
}
|
style: TextStyle(color: Colors.white),
|
||||||
},
|
),
|
||||||
),
|
)
|
||||||
ownReactionIndex != -1
|
: SizedBox(),
|
||||||
? Padding(
|
],
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
);
|
||||||
child: Text(
|
}).toList(),
|
||||||
message.ownReactions[ownReactionIndex].score.toString(),
|
),
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SizedBox(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a reaction to the message
|
||||||
void sendReaction(BuildContext context, String reactionType) {
|
void sendReaction(BuildContext context, String reactionType) {
|
||||||
channel.sendReaction(message.id, reactionType);
|
channel.sendReaction(message, reactionType);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeReaction(BuildContext context, String reactionType) {
|
/// Remove a reaction from the message
|
||||||
channel.deleteReaction(message.id, reactionType);
|
void removeReaction(BuildContext context, Reaction reaction) {
|
||||||
|
channel.deleteReaction(message, reaction);
|
||||||
Navigator.of(context).pop();
|
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
|
/// The stream notifying the state of queryMessage call
|
||||||
Stream<bool> get queryMessage => _queryMessageController.stream;
|
Stream<bool> get queryMessage => _queryMessageController.stream;
|
||||||
|
|
||||||
|
bool _paginationEnded = false;
|
||||||
|
|
||||||
/// Calls [channel.query] updating [queryMessage] stream
|
/// Calls [channel.query] updating [queryMessage] stream
|
||||||
void queryMessages() {
|
void queryMessages() {
|
||||||
|
if (_paginationEnded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_queryMessageController.add(true);
|
_queryMessageController.add(true);
|
||||||
|
|
||||||
String firstId;
|
String firstId;
|
||||||
@@ -67,6 +73,9 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((res) {
|
.then((res) {
|
||||||
|
if (res.messages.isEmpty) {
|
||||||
|
_paginationEnded = true;
|
||||||
|
}
|
||||||
_queryMessageController.add(false);
|
_queryMessageController.add(false);
|
||||||
}).catchError((e, stack) {
|
}).catchError((e, stack) {
|
||||||
_queryMessageController.addError(e, stack);
|
_queryMessageController.addError(e, stack);
|
||||||
@@ -75,8 +84,11 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
|
|
||||||
/// Calls [channel.getReplies] updating [queryMessage] stream
|
/// Calls [channel.getReplies] updating [queryMessage] stream
|
||||||
Future<void> getReplies(String parentId) async {
|
Future<void> getReplies(String parentId) async {
|
||||||
|
if (_paginationEnded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_queryMessageController.add(true);
|
_queryMessageController.add(true);
|
||||||
print('PARENT $parentId');
|
|
||||||
|
|
||||||
String firstId;
|
String firstId;
|
||||||
if (widget.channel.state.threads.containsKey(parentId)) {
|
if (widget.channel.state.threads.containsKey(parentId)) {
|
||||||
@@ -96,12 +108,16 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((res) {
|
.then((res) {
|
||||||
|
if (res.messages.isEmpty) {
|
||||||
|
_paginationEnded = true;
|
||||||
|
}
|
||||||
_queryMessageController.add(false);
|
_queryMessageController.add(false);
|
||||||
}).catchError((e, stack) {
|
}).catchError((e, stack) {
|
||||||
_queryMessageController.addError(e, stack);
|
_queryMessageController.addError(e, stack);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query the channel members and watchers
|
||||||
Future<void> queryMembersAndWatchers() async {
|
Future<void> queryMembersAndWatchers() async {
|
||||||
await widget.channel.query(
|
await widget.channel.query(
|
||||||
membersPagination: PaginationParams(
|
membersPagination: PaginationParams(
|
||||||
@@ -133,14 +149,18 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
initialData: widget.channel.state != null,
|
initialData: widget.channel.state != null,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || !snapshot.data) {
|
if (!snapshot.hasData || !snapshot.data) {
|
||||||
return Scaffold(
|
return Container(
|
||||||
body: Center(
|
height: 30,
|
||||||
|
child: Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
return Center(
|
return Container(
|
||||||
child: Text(snapshot.error),
|
height: 30,
|
||||||
|
child: Center(
|
||||||
|
child: Text(snapshot.error),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return widget.child;
|
return widget.child;
|
||||||
|
|||||||
+71
-92
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
|
|
||||||
@@ -59,47 +58,27 @@ class StreamChat extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamChatState extends State<StreamChat>
|
/// The current state of the StreamChat widget
|
||||||
with AutomaticKeepAliveClientMixin {
|
class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
|
||||||
Client get client => widget.client;
|
Client get client => widget.client;
|
||||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
Timer _disconnectTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
|
||||||
final theme = _getTheme(context, widget.streamChatThemeData);
|
final theme = _getTheme(context, widget.streamChatThemeData);
|
||||||
return StreamChatTheme(
|
return StreamChatTheme(
|
||||||
data: theme,
|
data: theme,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final materialTheme = Theme.of(context);
|
final materialTheme = Theme.of(context);
|
||||||
final streamChatTheme = StreamChatTheme.of(context);
|
final streamTheme = StreamChatTheme.of(context);
|
||||||
return Theme(
|
return Theme(
|
||||||
data: materialTheme.copyWith(
|
data: materialTheme.copyWith(
|
||||||
accentColor: streamChatTheme.accentColor,
|
primaryIconTheme: streamTheme.primaryIconTheme,
|
||||||
scaffoldBackgroundColor: streamChatTheme.backgroundColor,
|
accentColor: streamTheme.accentColor,
|
||||||
),
|
scaffoldBackgroundColor: streamTheme.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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
child: widget.child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -114,6 +93,7 @@ class StreamChatState extends State<StreamChat>
|
|||||||
final theme = defaultTheme.copyWith(
|
final theme = defaultTheme.copyWith(
|
||||||
primaryColor: themeData?.primaryColor,
|
primaryColor: themeData?.primaryColor,
|
||||||
defaultChannelImage: themeData?.defaultChannelImage,
|
defaultChannelImage: themeData?.defaultChannelImage,
|
||||||
|
primaryIconTheme: themeData?.primaryIconTheme,
|
||||||
defaultUserImage: themeData?.defaultUserImage,
|
defaultUserImage: themeData?.defaultUserImage,
|
||||||
backgroundColor: themeData?.backgroundColor,
|
backgroundColor: themeData?.backgroundColor,
|
||||||
channelTheme: defaultTheme.channelTheme.copyWith(
|
channelTheme: defaultTheme.channelTheme.copyWith(
|
||||||
@@ -174,86 +154,85 @@ class StreamChatState extends State<StreamChat>
|
|||||||
title: themeData?.channelPreviewTheme?.title,
|
title: themeData?.channelPreviewTheme?.title,
|
||||||
lastMessageAt: themeData?.channelPreviewTheme?.lastMessageAt,
|
lastMessageAt: themeData?.channelPreviewTheme?.lastMessageAt,
|
||||||
subtitle: themeData?.channelPreviewTheme?.subtitle,
|
subtitle: themeData?.channelPreviewTheme?.subtitle,
|
||||||
|
unreadCounterColor: themeData?.channelPreviewTheme?.unreadCounterColor,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return theme;
|
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
|
/// The current user
|
||||||
User get user => widget.client.state.user;
|
User get user => widget.client.state.user;
|
||||||
|
|
||||||
/// The current user as a stream
|
/// The current user as a stream
|
||||||
Stream<User> get userStream => widget.client.state.userStream;
|
Stream<User> get userStream => widget.client.state.userStream;
|
||||||
|
|
||||||
/// The current channel list
|
@override
|
||||||
final List<Channel> channels = [];
|
void initState() {
|
||||||
|
super.initState();
|
||||||
/// The current channel list as a stream
|
WidgetsBinding.instance.addObserver(this);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the current channel list
|
StreamSubscription _newMessageSubscription;
|
||||||
void clearChannels() {
|
|
||||||
channels.clear();
|
@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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscriptions.forEach((s) => s.cancel());
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_queryChannelsLoadingController.close();
|
|
||||||
_channelsController.close();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,16 @@ class StreamChatTheme extends InheritedWidget {
|
|||||||
|
|
||||||
/// Use this method to get the current [StreamChatThemeData] instance
|
/// Use this method to get the current [StreamChatThemeData] instance
|
||||||
static StreamChatThemeData of(BuildContext context) {
|
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
|
/// The widget that will be built when the user image is unavailable
|
||||||
final Widget Function(BuildContext, User) defaultUserImage;
|
final Widget Function(BuildContext, User) defaultUserImage;
|
||||||
|
|
||||||
|
/// Primary icon theme
|
||||||
|
final IconThemeData primaryIconTheme;
|
||||||
|
|
||||||
/// Create a theme from scratch
|
/// Create a theme from scratch
|
||||||
StreamChatThemeData({
|
StreamChatThemeData({
|
||||||
this.primaryColor,
|
this.primaryColor,
|
||||||
@@ -72,6 +84,7 @@ class StreamChatThemeData {
|
|||||||
this.ownMessageTheme,
|
this.ownMessageTheme,
|
||||||
this.defaultChannelImage,
|
this.defaultChannelImage,
|
||||||
this.defaultUserImage,
|
this.defaultUserImage,
|
||||||
|
this.primaryIconTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create a theme from a Material [Theme]
|
/// Create a theme from a Material [Theme]
|
||||||
@@ -80,6 +93,7 @@ class StreamChatThemeData {
|
|||||||
|
|
||||||
return defaultTheme.copyWith(
|
return defaultTheme.copyWith(
|
||||||
accentColor: theme.accentColor,
|
accentColor: theme.accentColor,
|
||||||
|
primaryIconTheme: theme.primaryIconTheme,
|
||||||
primaryColor: theme.colorScheme.primary,
|
primaryColor: theme.colorScheme.primary,
|
||||||
secondaryColor: theme.colorScheme.secondary,
|
secondaryColor: theme.colorScheme.secondary,
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
@@ -114,10 +128,12 @@ class StreamChatThemeData {
|
|||||||
MessageTheme otherMessageTheme,
|
MessageTheme otherMessageTheme,
|
||||||
Widget Function(BuildContext, Channel) defaultChannelImage,
|
Widget Function(BuildContext, Channel) defaultChannelImage,
|
||||||
Widget Function(BuildContext, User) defaultUserImage,
|
Widget Function(BuildContext, User) defaultUserImage,
|
||||||
|
IconThemeData primaryIconTheme,
|
||||||
}) =>
|
}) =>
|
||||||
StreamChatThemeData(
|
StreamChatThemeData(
|
||||||
primaryColor: primaryColor ?? this.primaryColor,
|
primaryColor: primaryColor ?? this.primaryColor,
|
||||||
secondaryColor: secondaryColor ?? this.secondaryColor,
|
secondaryColor: secondaryColor ?? this.secondaryColor,
|
||||||
|
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
||||||
accentColor: accentColor ?? this.accentColor,
|
accentColor: accentColor ?? this.accentColor,
|
||||||
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
|
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
|
||||||
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
|
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
|
||||||
@@ -190,6 +206,8 @@ class StreamChatThemeData {
|
|||||||
return StreamChatThemeData(
|
return StreamChatThemeData(
|
||||||
accentColor: accentColor,
|
accentColor: accentColor,
|
||||||
primaryColor: isDark ? Colors.black : Colors.white,
|
primaryColor: isDark ? Colors.black : Colors.white,
|
||||||
|
primaryIconTheme:
|
||||||
|
IconThemeData(color: isDark ? Colors.white : Colors.black),
|
||||||
defaultChannelImage: (context, channel) => SizedBox(),
|
defaultChannelImage: (context, channel) => SizedBox(),
|
||||||
backgroundColor: isDark ? Colors.black : Colors.white,
|
backgroundColor: isDark ? Colors.black : Colors.white,
|
||||||
defaultUserImage: (context, user) => Center(
|
defaultUserImage: (context, user) => Center(
|
||||||
@@ -199,6 +217,7 @@ class StreamChatThemeData {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
channelPreviewTheme: ChannelPreviewTheme(
|
channelPreviewTheme: ChannelPreviewTheme(
|
||||||
|
unreadCounterColor: Color(0xffd0021B),
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: BoxConstraints.tightFor(
|
constraints: BoxConstraints.tightFor(
|
||||||
@@ -423,12 +442,14 @@ class ChannelPreviewTheme {
|
|||||||
final TextStyle subtitle;
|
final TextStyle subtitle;
|
||||||
final TextStyle lastMessageAt;
|
final TextStyle lastMessageAt;
|
||||||
final AvatarTheme avatarTheme;
|
final AvatarTheme avatarTheme;
|
||||||
|
final Color unreadCounterColor;
|
||||||
|
|
||||||
const ChannelPreviewTheme({
|
const ChannelPreviewTheme({
|
||||||
this.title,
|
this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.lastMessageAt,
|
this.lastMessageAt,
|
||||||
this.avatarTheme,
|
this.avatarTheme,
|
||||||
|
this.unreadCounterColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
ChannelPreviewTheme copyWith({
|
ChannelPreviewTheme copyWith({
|
||||||
@@ -436,12 +457,14 @@ class ChannelPreviewTheme {
|
|||||||
TextStyle subtitle,
|
TextStyle subtitle,
|
||||||
TextStyle lastMessageAt,
|
TextStyle lastMessageAt,
|
||||||
AvatarTheme avatarTheme,
|
AvatarTheme avatarTheme,
|
||||||
|
Color unreadCounterColor,
|
||||||
}) =>
|
}) =>
|
||||||
ChannelPreviewTheme(
|
ChannelPreviewTheme(
|
||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
subtitle: subtitle ?? this.subtitle,
|
subtitle: subtitle ?? this.subtitle,
|
||||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
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
|
/// The message parent of this thread
|
||||||
final Message parent;
|
final Message parent;
|
||||||
|
|
||||||
|
/// Instantiate a new ThreadHeader
|
||||||
ThreadHeader({
|
ThreadHeader({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.parent,
|
@required this.parent,
|
||||||
@@ -129,9 +130,6 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
size: 15,
|
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
|
/// Widget to show the current list of typing users
|
||||||
class TypingIndicator extends StatelessWidget {
|
class TypingIndicator extends StatelessWidget {
|
||||||
|
/// Instantiate a new TypingIndicator
|
||||||
const TypingIndicator({
|
const TypingIndicator({
|
||||||
Key key,
|
Key key,
|
||||||
this.channel,
|
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_list_view.dart';
|
||||||
export 'src/channel_name.dart';
|
export 'src/channel_name.dart';
|
||||||
export 'src/channel_preview.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_input.dart';
|
||||||
export 'src/message_list_view.dart';
|
export 'src/message_list_view.dart';
|
||||||
|
export 'src/message_text.dart';
|
||||||
export 'src/message_widget.dart';
|
export 'src/message_widget.dart';
|
||||||
export 'src/reaction_picker.dart';
|
export 'src/reaction_picker.dart';
|
||||||
|
export 'src/reply_indicator.dart';
|
||||||
|
export 'src/sending_indicator.dart';
|
||||||
export 'src/stream_channel.dart';
|
export 'src/stream_channel.dart';
|
||||||
export 'src/stream_chat.dart';
|
export 'src/stream_chat.dart';
|
||||||
export 'src/stream_chat_theme.dart';
|
export 'src/stream_chat_theme.dart';
|
||||||
|
export 'src/system_message.dart';
|
||||||
export 'src/thread_header.dart';
|
export 'src/thread_header.dart';
|
||||||
export 'src/typing_indicator.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
|
name: stream_chat_flutter
|
||||||
homepage: https://github.com/GetStream/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.
|
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:
|
environment:
|
||||||
sdk: ">=2.3.0 <3.0.0"
|
sdk: ">=2.3.0 <3.0.0"
|
||||||
@@ -9,19 +9,21 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
rxdart: ^0.24.0
|
photo_view: ^0.9.2
|
||||||
|
rxdart: ^0.24.1
|
||||||
jiffy: ^3.0.1
|
jiffy: ^3.0.1
|
||||||
cached_network_image: ^2.1.0+1
|
flutter_portal: ^0.1.0
|
||||||
flutter_markdown: ^0.3.5
|
cached_network_image: ^2.2.0+1
|
||||||
url_launcher: ^5.4.2
|
flutter_markdown: ^0.4.2
|
||||||
video_player: ^0.10.8+1
|
url_launcher: ^5.4.11
|
||||||
|
video_player: ^0.10.11+1
|
||||||
chewie: ^0.9.10
|
chewie: ^0.9.10
|
||||||
file_picker: ^1.6.3+2
|
file_picker: ^1.12.0
|
||||||
image_picker: ^0.6.5
|
image_picker: ^0.6.7+2
|
||||||
flutter_keyboard_visibility: ^2.0.0
|
flutter_keyboard_visibility: ^3.2.1
|
||||||
stream_chat: ^0.1.28
|
stream_chat: ^0.2.0-alpha+23
|
||||||
mime: ^0.9.6+3
|
mime: ^0.9.6+3
|
||||||
visibility_detector: ^0.1.4
|
visibility_detector: ^0.1.5
|
||||||
http_parser: ^3.1.4
|
http_parser: ^3.1.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user