Merge pull request #25 from GetStream/feature/notifications

Offline storage and notifications
This commit is contained in:
Salvatore Giordano
2020-07-06 15:07:15 +02:00
committed by GitHub
59 changed files with 4048 additions and 1719 deletions
+104 -29
View File
@@ -1,60 +1,135 @@
## 0.1.35 ## 0.2.1-alpha+11
- Add backgroundColor as part of StreamChatThemeData - Update llc dependency
## 0.1.34 ## 0.2.1-alpha+10
- Add `onUserAvatarTap` - Update llc dependency
## 0.1.33 ## 0.2.1-alpha+9
- Add default user and channel image to `StreamChatTheme` - Add read indicators
- Update llc dependency
## 0.1.32 ## 0.2.1-alpha+8
- Update llc version - User queryMembers for mentions
## 0.1.31 ## 0.2.1-alpha+7
- Add `initialMessage` property to `MessageInput` - Update llc dependency
## 0.1.30 ## 0.2.1-alpha+6
- Add simple rendering of file attachments - Update llc dependency
- Minor bugfix
## 0.1.29 ## 0.2.1-alpha+4
- Add `doImageUploadRequest` and `doFileUploadRequest` to `MessageInput` to let users customize file uploading (eg: custom cdn) - Update llc dependency
## 0.1.28 - Add system messages
- Add `onTap` to `ChannelImage` ## 0.2.1-alpha+3
- Add `onImageTap` and `onTitleTap` to `ChannelHeader` - Update llc dependency
- Add `onImageTap` to `ChannelListView` - Fix hero tag generation for attachment
## 0.1.27 ## 0.2.1-alpha+2
- Show other member user's name and image in one to one channels - Fixed reactions bubble going below other messages
- Updated llc dependency
## 0.1.25 ## 0.2.1-alpha+1
- Removed the additional `Navigator` in `StreamChat` widget.
It was added to make the app have the `StreamChat` widget as ancestor in every route.
Now the recommended way to add `StreamChat` to your app is using the `builder` property of your `MaterialApp` widget.
Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to every route of your app.
Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information.
```dart
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
builder: (context, widget) {
return StreamChat(
child: widget,
client: client,
);
},
home: ChannelListPage(),
);
```
- Fix reaction bubble going below previous message on iOS
- Fix message list view reloading messages even if the pagination is ended
## 0.2.1-alpha
- New message widget
- Moved some properties from `MessageListView` to `MessageWidget`
- Added `MessageDetails` property to `MessageBuilder`
- Added example to customize the message using `MessageWidget` (`customize_message_widget.dart`)
## 0.2.0-alpha+15
- Add background color in StreamChatTheme
## 0.2.0-alpha+13
- Handle channel deleted event
## 0.2.0-alpha+11
- Fix message builder and add messageList to it
## 0.2.0-alpha+10
- Add date divider builder
- Fix reply indicator tap
## 0.2.0-alpha+9
- Add `attachmentBuilders` to `MessageWidget` and `MessageListView`
## 0.2.0-alpha+7
- Update llc dependency
## 0.2.0-alpha+5
- Remove dependencies on notification service
- Expose some helping method for integrate offline storage with push notifications
## 0.2.0-alpha+3
- Fix overflow in mentions overlay - 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
+10 -3
View File
@@ -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
@@ -156,6 +157,12 @@ Out of the box, all chat widgets use their default styling, and there are two wa
} }
} }
``` ```
### Offline storage
By default the library saves information about channels and messages in a SQLite DB.
Set the property `persistenceEnabled` to false if you don't want to use the offline storage.
## Contributing ## Contributing
+2 -2
View File
@@ -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
+3
View File
@@ -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"))
}
}
+1
View File
@@ -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'
} }
} }
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Notifications</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,182 @@
//
// NotificationService.swift
// Notifications
//
// Created by Salvatore Giordano on 25/03/2020.
// Copyright © 2020 The Chromium Authors. All rights reserved.
//
import UserNotifications
import StreamChatClient
final class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let sharedDefaults = UserDefaults(suiteName: "group.io.stream.flutter"),
let apiKey = sharedDefaults.string(forKey: "KEY_API_KEY"),
let userId = sharedDefaults.string(forKey: "KEY_USER_ID"),
let token = sharedDefaults.string(forKey: "KEY_TOKEN"),
let messageId = bestAttemptContent?.userInfo["message_id"] as? String else {
return
}
Client.config = .init(apiKey: apiKey, logOptions: .error)
Client.shared.set(user: User(id: userId), token: token) { res in
guard res.isConnected else {
return
}
Client.shared.message(withId: messageId) { [weak self] res in
if let message = res.value?.message,
let channel = res.value?.channel {
let messageWrapper = MessageWrapper(channel: channel, message: message)
if let encodedData = try? JSONEncoder.stream.encode(messageWrapper),
let encodedString = String(data: encodedData, encoding: .utf8) {
let storedMessages = sharedDefaults.stringArray(forKey: "messageQueue") ?? []
sharedDefaults.setValue(storedMessages + [encodedString], forKey: "messageQueue")
// Modify the notification content here...
self?.bestAttemptContent?.title = "[modified] \(self?.bestAttemptContent?.title ?? "<NoContent>")"
contentHandler(self?.bestAttemptContent ?? request.content)
}
Client.shared.disconnect()
}
}
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
public struct MessageWrapper: Encodable {
private enum CodingKeys: String, CodingKey {
case id
case channel
case type
case user
case created = "created_at"
case updated = "updated_at"
case text
case command
case args
case attachments
case parentId = "parent_id"
case showReplyInChannel = "show_in_channel"
case mentionedUsers = "mentioned_users"
}
init(channel: Channel, message: Message) {
id = message.id
type = message.type
user = message.user
created = message.created
updated = message.updated
text = message.text
command = message.command
args = message.args
attachments = message.attachments
parentId = message.parentId
showReplyInChannel = message.showReplyInChannel
mentionedUsers = message.mentionedUsers
extraData = message.extraData
self.channel = ChannelWrapper(channel: channel)
}
/// A message id.
public let id: String
/// The channel cid.
public let channel: ChannelWrapper?
/// A message type (see `MessageType`).
public let type: MessageType
/// A user (see `User`).
public let user: User
/// A created date.
public let created: Date
/// A updated date.
public let updated: Date
/// A text.
public let text: String
/// A used command name.
public let command: String?
/// A used command args.
public let args: String?
/// Attachments (see `Attachment`).
public let attachments: [Attachment]
/// A parent message id.
public let parentId: String?
/// Check if this reply message needs to show in the channel.
public let showReplyInChannel: Bool
/// Mentioned users (see `User`).
public let mentionedUsers: [User]
/// An extra data for the message.
public let extraData: Codable?
}
public struct ChannelWrapper: Encodable {
/// Coding keys for the encoding.
private enum CodingKeys: String, CodingKey {
case id
case cid
case type
case name
case imageURL = "image"
case members
case lastMessageDate = "last_message_at"
case createdBy = "created_by"
case created = "created_at"
case deleted = "deleted_at"
case frozen
}
init(channel: Channel) {
id = channel.id
cid = channel.cid
type = channel.type
name = channel.name
imageURL = channel.imageURL
lastMessageDate = channel.lastMessageDate
created = channel.created
deleted = channel.deleted
createdBy = channel.createdBy
config = channel.config
frozen = channel.frozen
extraData = channel.extraData
}
/// A channel id.
public let id: String
/// A channel type + id.
public let cid: ChannelId
/// A channel type.
public let type: ChannelType
/// A channel name.
public let name: String?
/// An image of the channel.
public let imageURL: URL?
/// The last message date.
public let lastMessageDate: Date?
/// A channel created date.
public let created: Date
/// A channel deleted date.
public let deleted: Date?
/// A creator of the channel.
public let createdBy: User?
/// A config.
public let config: Channel.Config
/// Checks if the channel is frozen.
public let frozen: Bool
/// A list of user ids of the channel members.
public let members = Set<Member>()
/// An extra data for the channel.
public let extraData: Codable?
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.stream.flutter</string>
</array>
</dict>
</plist>
+2 -2
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # 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
View File
@@ -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
+215 -17
View File
@@ -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 = (
+35 -7
View File
@@ -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")
}
}
} }
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.io.stream.flutter</string>
</array>
</dict>
</plist>
+25 -19
View File
@@ -16,13 +16,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// or to retrieve outer scope needed such as messages from the [Channel.state]. /// 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;
+27 -26
View File
@@ -20,13 +20,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// You can perform these more granular style changes using [StreamChatTheme.copyWith]. /// 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 -20
View File
@@ -21,13 +21,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// - We retrieve the count of unread messages from [Channel.state] /// - 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(
+133
View File
@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Fifth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/)
///
/// Customizing how messages are rendered is another very common use-case that the SDK supports easily.
///
/// Replace the built-in message component with your own is done by passing it as a builder function to the [MessageListView] widget.
///
/// The message builder function will get the usual [BuildContext] argument as well as the [Message] object and its position inside the list.
///
/// If you look at the code you can see that we use [StreamChat.of] to retrieve the current user so that we can style messages own messages in a different way.
///
/// Since custom widgets and builders are always children of [StreamChat] or part of a [Channel],
/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly
/// or to retrieve outer scope needed such as messages from the [Channel.state].
void main() async {
final client = Client(
's2dxdhpxd94g',
logLevel: Level.INFO,
);
await client.setUser(
User(id: 'super-band-9'),
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A',
);
runApp(MyApp(client));
}
class MyApp extends StatelessWidget {
final Client client;
MyApp(this.client);
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
child: child,
client: client,
),
home: Container(
child: ChannelListPage(),
),
);
}
}
class ChannelListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: {
'members': {
'\$in': [StreamChat.of(context).user.id],
}
},
sort: [SortOption('last_message_at')],
pagination: PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
),
),
);
}
}
class ChannelPage extends StatelessWidget {
const ChannelPage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
messageBuilder: _messageBuilder,
),
),
MessageInput(),
],
),
);
}
Widget _messageBuilder(
BuildContext context,
MessageDetails details,
List<Message> messages,
) {
final message = details.message;
final color = details.isMyMessage ? Colors.blueGrey : Colors.blue;
if (message.isSystem) {
return SizedBox();
}
return MessageWidget(
message: message,
messageTheme: details.isMyMessage
? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme,
borderSide: BorderSide(
color: color,
width: 2,
),
padding: const EdgeInsets.all(2),
attachmentBorderSide: BorderSide(
color: color,
width: 2,
),
attachmentPadding: EdgeInsets.all(8),
borderRadiusGeometry: BorderRadius.vertical(
top: !details.isLastUser ? Radius.circular(16) : Radius.zero,
bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero,
),
showSendingIndicator: DisplayWidget.gone,
reverse: false,
showUserAvatar:
details.isNextUser ? DisplayWidget.hide : DisplayWidget.show,
showTimestamp: !details.isNextUser,
showUsername: !details.isNextUser,
showReactions: false,
showReplyIndicator: false,
);
}
}
+85 -15
View File
@@ -1,10 +1,75 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/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(),
), ),
); );
} }
+19 -18
View File
@@ -20,13 +20,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// [ChannelListView] handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel. /// [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(),
), ),
); );
} }
+3 -3
View File
@@ -26,13 +26,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// If you now run the simulator you will see a single channel UI. /// 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
View File
@@ -11,13 +11,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same [ThreadPage]. /// 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(),
), ),
); );
} }
+2
View File
@@ -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:
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class AttachmentActions extends StatelessWidget {
final Attachment attachment;
final Message message;
const AttachmentActions({
Key key,
this.attachment,
this.message,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: attachment.actions?.map((action) {
if (action.style == 'primary') {
return FlatButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Text('${action.text}'),
color: action.style == 'primary'
? StreamChatTheme.of(context).accentColor
: null,
textColor: Colors.white,
onPressed: () {
streamChannel.channel.sendAction(message, {
action.name: action.value,
});
},
);
}
return OutlineButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Text('${action.text}'),
color: StreamChatTheme.of(context).accentColor,
onPressed: () {
streamChannel.channel.sendAction(message, {
action.name: action.value,
});
},
);
})?.toList(),
);
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
class AttachmentError extends StatelessWidget {
final Attachment attachment;
final Size size;
const AttachmentError({
Key key,
@required this.attachment,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.localUri != null) {
return Image.file(
File(attachment.localUri.path),
);
}
return Center(
child: Container(
width: size?.width,
height: size?.height,
color: Color(0xffd0021B).withOpacity(.1),
child: Center(
child: Icon(
Icons.error_outline,
color: Colors.white,
),
),
),
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_theme.dart';
import 'utils.dart';
class AttachmentTitle extends StatelessWidget {
const AttachmentTitle({
Key key,
@required this.attachment,
@required this.messageTheme,
}) : super(key: key);
final MessageTheme messageTheme;
final Attachment attachment;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (attachment.titleLink != null) {
launchURL(context, attachment.titleLink);
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText.copyWith(
color: StreamChatTheme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
style: messageTheme.messageText,
),
],
),
),
);
}
}
-3
View File
@@ -157,9 +157,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
child: Icon( 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,
), ),
), ),
); );
+1
View File
@@ -43,6 +43,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// 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,
+70 -18
View File
@@ -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();
}
} }
+1
View File
@@ -8,6 +8,7 @@ import 'stream_channel.dart';
/// ///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates. /// 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,
+18 -15
View File
@@ -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') {
+144
View File
@@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
/// Widget dedicated to the management of a channel list with pagination
class ChannelsBloc extends StatefulWidget {
/// The widget child
final Widget child;
/// Instantiate a new ChannelsBloc
const ChannelsBloc({
Key key,
this.child,
}) : super(key: key);
@override
ChannelsBlocState createState() => ChannelsBlocState();
/// Use this method to get the current [ChannelsBlocState] instance
static ChannelsBlocState of(BuildContext context) {
ChannelsBlocState streamChatState;
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
if (streamChatState == null) {
throw Exception('You must have a ChannelsBloc widget as anchestor');
}
return streamChatState;
}
}
/// The current state of the [ChannelsBloc]
class ChannelsBlocState extends State<ChannelsBloc>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
/// The current channel list
List<Channel> get channels => _channelsController.value;
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController =
BehaviorSubject.seeded([]);
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
_queryChannelsLoadingController.stream;
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
}) async {
if (_queryChannelsLoadingController.value == true) {
return;
}
_queryChannelsLoadingController.sink.add(true);
try {
final clear = paginationParams == null ||
paginationParams.offset == null ||
paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels);
StreamChat.of(context)
.client
.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
onlyOffline: onlyOffline,
)
.listen((channels) {
if (clear) {
_channelsController.add(channels);
} else {
final l = oldChannels + channels;
_channelsController.add(l);
}
}, onDone: () {
_queryChannelsLoadingController.sink.add(false);
}, onError: (err, stackTrace) {
print(err);
print(stackTrace);
_queryChannelsLoadingController.addError(err, stackTrace);
});
} catch (err, stackTrace) {
_queryChannelsLoadingController.addError(err, stackTrace);
}
}
final List<StreamSubscription> _subscriptions = [];
@override
void initState() {
super.initState();
final client = StreamChat.of(context).client;
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
_channelsController.add(newChannels);
}
}));
_subscriptions.add(client
.on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
.listen((e) {
final channel = e.channel;
_channelsController
.add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
}));
}
@override
void dispose() {
_channelsController.close();
_queryChannelsLoadingController.close();
_subscriptions.forEach((s) => s.cancel());
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
/// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget {
final DateTime dateTime;
const DateDivider({
Key key,
@required this.dateTime,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final divider = Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Divider(),
),
);
final createdAt = Jiffy(dateTime);
final now = DateTime.now();
final hourInfo = createdAt.format('h:mm a');
String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'TODAY';
} else if (Jiffy(createdAt)
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
dayInfo = 'YESTERDAY';
} else if (Jiffy(createdAt).isAfter(
now.subtract(Duration(days: 7)),
Units.DAY,
)) {
dayInfo = createdAt.format('EEEE').toUpperCase();
} else if (Jiffy(createdAt).isAfter(
Jiffy(now).subtract(years: 1),
Units.DAY,
)) {
dayInfo = createdAt.format('dd/MM').toUpperCase();
} else {
dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
}
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
divider,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: dayInfo,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: ' AT'),
TextSpan(text: ' $hourInfo'),
],
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
style: TextStyle(
fontSize: 10,
color:
Theme.of(context).textTheme.headline6.color.withOpacity(.5),
),
),
),
divider,
],
);
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
class DeletedMessage extends StatelessWidget {
const DeletedMessage({
Key key,
@required this.messageTheme,
}) : super(key: key);
final MessageTheme messageTheme;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'This message was deleted...',
style: messageTheme.messageText.copyWith(
fontStyle: FontStyle.italic,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
);
}
}
+32
View File
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/utils.dart';
class FileAttachment extends StatelessWidget {
final Attachment attachment;
final Size size;
const FileAttachment({
Key key,
@required this.attachment,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: InkWell(
onTap: () {
launchURL(context, attachment.assetUrl);
},
child: Container(
width: size?.width ?? 100,
height: size?.height ?? 100,
child: Center(
child: Icon(Icons.attach_file),
),
),
),
);
}
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
/// A full screen image widget
class FullScreenImage extends StatelessWidget {
/// The url of the image
final String url;
/// Instantiate a new FullScreenImage
const FullScreenImage({
Key key,
@required this.url,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: PhotoView(
imageProvider: CachedNetworkImageProvider(url),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: url,
),
),
);
}
}
+78
View File
@@ -0,0 +1,78 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'utils.dart';
class FullScreenVideo extends StatefulWidget {
final Attachment attachment;
FullScreenVideo({
Key key,
@required this.attachment,
}) : super(key: key);
@override
_FullScreenVideoState createState() => _FullScreenVideoState();
}
class _FullScreenVideoState extends State<FullScreenVideo> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
key: _scaffoldKey,
builder: (context) {
if (!initialized) {
return Center(
child: CircularProgressIndicator(),
);
}
return Chewie(
controller: _chewieController,
);
},
),
);
}
@override
void initState() {
super.initState();
_videoPlayerController =
VideoPlayerController.network(widget.attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
setState(() {
initialized = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
});
});
VoidCallback errorListener;
errorListener = () {
if (_videoPlayerController.value.hasError) {
Navigator.pop(context);
launchURL(_scaffoldKey.currentContext, widget.attachment.titleLink);
}
_videoPlayerController.removeListener(errorListener);
};
_videoPlayerController.addListener(errorListener);
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}
+93
View File
@@ -0,0 +1,93 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment_actions.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
import 'full_screen_image.dart';
class GiphyAttachment extends StatelessWidget {
final Attachment attachment;
final MessageTheme messageTheme;
final Message message;
final Size size;
const GiphyAttachment({
Key key,
this.attachment,
this.messageTheme,
this.message,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.thumbUrl == null &&
attachment.imageUrl == null &&
attachment.assetUrl == null) {
return AttachmentError(
attachment: attachment,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Stack(
children: <Widget>[
GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return FullScreenImage(
url: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl,
);
}));
},
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
),
),
],
),
if (attachment.title != null)
Container(
alignment: Alignment.bottomCenter,
child: Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
),
if (attachment.actions != null)
AttachmentActions(
attachment: attachment,
message: message,
),
],
);
}
}
+99
View File
@@ -0,0 +1,99 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
import 'full_screen_image.dart';
import 'utils.dart';
class ImageAttachment extends StatelessWidget {
final Attachment attachment;
final Message message;
final MessageTheme messageTheme;
final Size size;
const ImageAttachment({
Key key,
@required this.attachment,
@required this.message,
this.messageTheme,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.thumbUrl == null &&
attachment.imageUrl == null &&
attachment.assetUrl == null) {
return AttachmentError(
attachment: attachment,
);
}
return SizedBox.fromSize(
size: size,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return FullScreenImage(
url: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl,
);
}));
},
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
),
),
),
if (attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
],
),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => launchURL(
context,
attachment.titleLink ?? attachment.ogScrapeUrl,
),
),
),
),
],
),
);
}
}
+209
View File
@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
import '../stream_chat_flutter.dart';
import 'message_input.dart';
import 'stream_chat.dart';
class MessageActionsBottomSheet extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder;
final void Function(Message) onThreadTap;
final Message message;
final bool showReactions;
final bool showDeleteMessage;
final bool showEditMessage;
final bool showReply;
final Map<String, String> reactionToEmoji = const {
'love': '❤️',
'haha': '😂',
'like': '👍',
'sad': '😕',
'angry': '😡',
'wow': '😲',
};
const MessageActionsBottomSheet({
Key key,
this.message,
this.showReactions,
this.showDeleteMessage,
this.showEditMessage,
this.onThreadTap,
this.showReply,
this.editMessageInputBuilder,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactions &&
(message.status == MessageSendingStatus.SENT ||
message.status == null))
ReactionPicker(
channel: channel,
reactionToEmoji: reactionToEmoji,
message: message,
),
if (showDeleteMessage) _buildDeleteButton(context),
if (showEditMessage) _buildEditMessage(context),
if (showReply &&
(message.status == MessageSendingStatus.SENT ||
message.status == null) &&
message.parentId == null)
_buildReplyButton(context),
],
),
);
}
FlatButton _buildDeleteButton(BuildContext context) {
return FlatButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Delete message',
style:
Theme.of(context).textTheme.headline5.copyWith(color: Colors.red),
),
),
onPressed: () {
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
);
}
FlatButton _buildEditMessage(BuildContext context) {
return FlatButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Edit message',
style: Theme.of(context).textTheme.headline5,
),
),
onPressed: () async {
Navigator.pop(context);
_showEditBottomSheet(context);
},
);
}
void _showEditBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showModalBottomSheet(
context: context,
elevation: 2,
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
builder: (context) {
return StreamChannel(
channel: channel,
child: Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: 16.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Edit message',
style: Theme.of(context).textTheme.headline6,
),
Container(
height: 30,
padding: const EdgeInsets.all(2.0),
child: AspectRatio(
aspectRatio: 1,
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
Navigator.of(context).pop();
},
fillColor:
Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.1)
: Colors.black.withOpacity(.1),
padding: EdgeInsets.all(4),
child: Icon(
Icons.close,
size: 15,
color: StreamChatTheme.of(context)
.primaryIconTheme
.color,
),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: editMessageInputBuilder != null
? editMessageInputBuilder(context, message)
: MessageInput(
editMessage: message,
onMessageSent: (_) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
},
),
),
],
),
);
},
);
}
FlatButton _buildReplyButton(BuildContext context) {
return FlatButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Start a thread',
style: Theme.of(context).textTheme.headline5,
),
),
onPressed: () {
Navigator.pop(context);
if (onThreadTap != null) {
onThreadTap(message);
}
},
);
}
}
+273 -176
View File
@@ -17,6 +17,21 @@ import '../stream_chat_flutter.dart';
import 'stream_channel.dart'; 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
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png)
@@ -61,9 +76,11 @@ typedef FileUploader = Future<String> Function(File, Channel);
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// 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
View File
@@ -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;
}
}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png)
/// ///
@@ -55,6 +94,7 @@ typedef ThreadTapCallback = void Function(Message, Widget);
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. /// 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 ||
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_theme.dart';
import 'utils.dart';
class MessageText extends StatelessWidget {
const MessageText({
Key key,
@required this.message,
@required this.messageTheme,
this.onMentionTap,
}) : super(key: key);
final Message message;
final void Function(User) onMentionTap;
final MessageTheme messageTheme;
@override
Widget build(BuildContext context) {
final text = _replaceMentions(message.text);
return MarkdownBody(
data: text,
onTapLink: (link) {
if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhere(
(u) => '@${u.name.replaceAll(' ', '')}' == link,
orElse: () => null,
);
if (onMentionTap != null) {
onMentionTap(mentionedUser);
} else {
print('tap on ${mentionedUser.name}');
}
} else {
launchURL(context, link);
}
},
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.apply(
bodyColor: messageTheme.messageText.color,
decoration: messageTheme.messageText.decoration,
decorationColor: messageTheme.messageText.decorationColor,
decorationStyle: messageTheme.messageText.decorationStyle,
fontFamily: messageTheme.messageText.fontFamily,
),
),
).copyWith(
p: messageTheme.messageText,
),
);
}
String _replaceMentions(String text) {
message.mentionedUsers?.forEach((u) {
text = text.replaceAll(
'@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
});
return text;
}
}
File diff suppressed because it is too large Load Diff
+47 -41
View File
@@ -24,56 +24,62 @@ class ReactionPicker extends StatelessWidget {
@override @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();
} }
} }
+56
View File
@@ -0,0 +1,56 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A reply button indicator
class ReplyIndicator extends StatelessWidget {
final Message message;
final VoidCallback onTap;
final bool reversed;
final MessageTheme messageTheme;
const ReplyIndicator({
Key key,
this.message,
this.onTap,
this.reversed = false,
this.messageTheme,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var row = [
Text(
'Replies: ${message.replyCount}',
style: messageTheme?.replies,
),
Transform(
transform: Matrix4.rotationY(reversed ? 0 : pi),
alignment: Alignment.center,
child: Icon(
Icons.subdirectory_arrow_left,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white12
: Colors.black12,
),
),
];
if (!reversed) {
row = row.reversed.toList();
}
return GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
),
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget {
final Message message;
const SendingIndicator({
Key key,
this.message,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (message.status == MessageSendingStatus.SENT || message.status == null) {
return CircleAvatar(
radius: 4,
backgroundColor: StreamChatTheme.of(context).accentColor,
child: Icon(
Icons.done,
color: Colors.white,
size: 4,
),
);
}
if (message.status == MessageSendingStatus.SENDING ||
message.status == MessageSendingStatus.UPDATING) {
return CircleAvatar(
radius: 4,
backgroundColor: Colors.grey,
child: Icon(
Icons.access_time,
size: 4,
color: Colors.white,
),
);
}
if (message.status == MessageSendingStatus.FAILED ||
message.status == MessageSendingStatus.FAILED_UPDATE ||
message.status == MessageSendingStatus.FAILED_DELETE) {
return CircleAvatar(
radius: 4,
backgroundColor: Color(0xffd0021B).withOpacity(.1),
child: Icon(
Icons.error_outline,
size: 4,
color: Colors.white,
),
);
}
return SizedBox();
}
}
+25 -5
View File
@@ -50,8 +50,14 @@ class StreamChannelState extends State<StreamChannel> {
/// The stream notifying the state of queryMessage call /// 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
View File
@@ -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 -1
View File
@@ -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,
); );
} }
+100
View File
@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows a date divider depending on the date difference
class SystemMessage extends StatelessWidget {
final Message message;
const SystemMessage({
Key key,
@required this.message,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final divider = Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Divider(),
),
);
final createdAt = Jiffy(message.createdAt.toLocal());
final now = DateTime.now();
final hourInfo = createdAt.format('h:mm a');
String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'TODAY';
} else if (Jiffy(createdAt)
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
dayInfo = 'YESTERDAY';
} else if (Jiffy(createdAt).isAfter(
now.subtract(Duration(days: 7)),
Units.DAY,
)) {
dayInfo = createdAt.format('EEEE').toUpperCase();
} else if (Jiffy(createdAt).isAfter(
Jiffy(now).subtract(years: 1),
Units.DAY,
)) {
dayInfo = createdAt.format('dd/MM').toUpperCase();
} else {
dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
divider,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
message.text,
style: TextStyle(
fontSize: 10,
color: Theme.of(context)
.textTheme
.headline6
.color
.withOpacity(.5),
fontWeight: FontWeight.bold,
),
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: dayInfo,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: ' AT'),
TextSpan(text: ' $hourInfo'),
],
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
style: TextStyle(
fontSize: 10,
color: Theme.of(context)
.textTheme
.headline6
.color
.withOpacity(.5),
),
),
],
),
),
divider,
],
);
}
}
+1 -3
View File
@@ -62,6 +62,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// The message parent of this thread /// 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,
), ),
), ),
), ),
+1
View File
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/stream_channel.dart';
/// Widget to show the current list of typing users /// 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,
+28
View File
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
class UnreadIndicator extends StatelessWidget {
const UnreadIndicator({
Key key,
@required this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: CircleAvatar(
backgroundColor:
StreamChatTheme.of(context).channelPreviewTheme.unreadCounterColor,
radius: 6,
child: Text(
'${channel.state.unreadCount}',
style: TextStyle(fontSize: 8),
),
),
);
}
}
+14
View File
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> launchURL(BuildContext context, String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Cannot launch the url'),
),
);
}
}
+155
View File
@@ -0,0 +1,155 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/full_screen_video.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
class VideoAttachment extends StatefulWidget {
final Attachment attachment;
final MessageTheme messageTheme;
final Size size;
VideoAttachment({
Key key,
@required this.attachment,
@required this.messageTheme,
this.size,
}) : super(key: key);
@override
_VideoAttachmentState createState() => _VideoAttachmentState();
}
class _VideoAttachmentState extends State<VideoAttachment> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
@override
Widget build(BuildContext context) {
if (!initialized) {
return Container(
height: widget.size?.height ?? 100,
width: widget.size?.width ?? 100,
child: Center(
child: CircularProgressIndicator(),
),
);
}
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: true,
showControls: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
errorBuilder: (_, e) {
if (widget.attachment.thumbUrl != null) {
return Stack(
children: <Widget>[
Container(
height: widget.size?.height,
width: widget.size?.width,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
widget.attachment.thumbUrl,
),
),
),
),
if (widget.attachment.titleLink != null)
Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
launchURL(context, widget.attachment.titleLink),
),
),
],
);
}
return AttachmentError(
attachment: widget.attachment,
size: widget.size,
);
});
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => FullScreenVideo(
attachment: widget.attachment,
),
),
);
},
child: Container(
height: widget.size?.height,
width: widget.size?.width,
child: Flex(
direction: Axis.vertical,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: FittedBox(
fit: BoxFit.cover,
child: Stack(
children: <Widget>[
Chewie(
controller: _chewieController,
),
Positioned.fill(
child: Center(
child: Material(
shape: CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Icon(Icons.play_arrow),
),
),
),
),
],
),
),
),
if (widget.attachment.title != null)
Material(
child: AttachmentTitle(
messageTheme: widget.messageTheme,
attachment: widget.attachment,
),
),
],
),
),
);
}
@override
void initState() {
super.initState();
_videoPlayerController =
VideoPlayerController.network(widget.attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
setState(() {
initialized = true;
});
});
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}
+13
View File
@@ -5,12 +5,25 @@ export 'src/channel_image.dart';
export 'src/channel_list_view.dart'; export 'src/channel_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
View File
@@ -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: