diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f5ca809..ec65f282 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,9 +6,6 @@ on: - master - feature/new-ui -env: - flutter_version: "1.20.2" - jobs: build_and_deploy_ios: runs-on: [macos-latest] @@ -28,7 +25,7 @@ jobs: bundle install - uses: subosito/flutter-action@v1.3.2 with: - flutter-version: ${{ env.flutter_version }} + channel: 'stable' - name: Install firebase-tools run: npm install -g firebase-tools - name: Flutter setup @@ -47,25 +44,23 @@ jobs: build_and_deploy_android: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 - with: - java-version: '12.x' - - uses: subosito/flutter-action@v1.3.2 - with: - flutter-version: ${{ env.flutter_version }} - - run: | - cd example - flutter pub get - - name: Build - run: | - cd example - flutter build apk - - name: Deploy - uses: wzieba/Firebase-Distribution-Github-Action@v1.2.1 - with: - appId: ${{secrets.FIREBASE_ANDROID_APPID}} - token: ${{secrets.FIREBASE_TOKEN}} - groups: stream-testers - file: example/build/app/outputs/apk/release/app-release.apk + - uses: actions/checkout@v2 + - name: Flutter action + uses: subosito/flutter-action@v1.3.2 + with: + channel: 'stable' + - name: Get dependencies + run: flutter pub get + - name: Coverage fix + run: | + file=test/coverage_helper_test.dart + echo "// Helper file to make coverage work for all dart files\n" > $file + echo "// ignore_for_file: unused_import" >> $file + find lib -name '*.dart' | grep -e '[^g]\.dart' | grep -v '_html.dart' | cut -c4- | awk -v package=stream_chat_flutter '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file + echo "" >> $file + echo "void main(){}" >> $file + cat $file + - name: Run tests + run: flutter test --coverage + - name: Codecov + run: bash <(curl -s https://codecov.io/bash) -c -t ${{ secrets.CODECOV_TOKEN }} -f coverage/lcov.info -F flutter_tool diff --git a/CHANGELOG.md b/CHANGELOG.md index cc19a470..95b92815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +## 0.2.8+3 + +- Add simple example of channel creation in sample app +- Add back button to the full-screen video view +- Update llc version + +## 0.2.8+2 + +- Add back button to the full-screen view + +## 0.2.8+1 + +- Update LLC dependency +- Update file_picker dependency + +## 0.2.8 + +- Update LLC dependency + +## 0.2.7+2 + +- Fix channellistview loading when client is not initialized +- Update LLC dependency + +## 0.2.7 + +- Update llc dependency +- Fixed a bug that made the SDK crash if it went to background while not connected + +## 0.2.6+1 + +- Update llc dependency + +## 0.2.6 + +- Add `pullToRefresh` property to `ChannelListView` +- Add `onLinkTap` to `MessageWidget` + +## 0.2.5 + +- Implement `didUpdateWidget` in `ChannelListView` to react to setState + ## 0.2.4 - Update llc dependency diff --git a/README.md b/README.md index d2bd5dd6..7ca34145 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ [![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter) ![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square) [![Gitter](https://badges.gitter.im/GetStream/stream-chat-flutter.svg)](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +![CI](https://github.com/GetStream/stream-chat-flutter/workflows/CI/badge.svg?branch=master) +[![codecov](https://codecov.io/gh/GetStream/stream-chat-flutter/branch/master/graph/badge.svg)](https://codecov.io/gh/GetStream/stream-chat-flutter) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id new file mode 100644 index 00000000..ab2c77bd --- /dev/null +++ b/example/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +13ecb9b157bb3e3d1ca2efb15c813a88 \ No newline at end of file diff --git a/example/ios/Notifications/NotificationService.swift b/example/ios/Notifications/NotificationService.swift index 7d085536..60e44390 100644 --- a/example/ios/Notifications/NotificationService.swift +++ b/example/ios/Notifications/NotificationService.swift @@ -7,7 +7,7 @@ // import UserNotifications -import StreamChatClient +//import StreamChatClient final class NotificationService: UNNotificationServiceExtension { @@ -26,29 +26,29 @@ final class NotificationService: UNNotificationServiceExtension { return } - Client.config = .init(apiKey: apiKey, logOptions: .error) - Client.shared.set(user: User(id: userId), token: token) { res in - guard res.isConnected else { - return - } - - Client.shared.message(withId: messageId) { [weak self] res in - if let message = res.value?.message, - let channel = res.value?.channel { - let messageWrapper = MessageWrapper(channel: channel, message: message) - if let encodedData = try? JSONEncoder.stream.encode(messageWrapper), - let encodedString = String(data: encodedData, encoding: .utf8) { - let storedMessages = sharedDefaults.stringArray(forKey: "messageQueue") ?? [] - sharedDefaults.setValue(storedMessages + [encodedString], forKey: "messageQueue") - - // Modify the notification content here... - self?.bestAttemptContent?.title = "[modified] \(self?.bestAttemptContent?.title ?? "")" - contentHandler(self?.bestAttemptContent ?? request.content) - } - Client.shared.disconnect() - } - } - } +// Client.config = .init(apiKey: apiKey, logOptions: .error) +// Client.shared.set(user: User(id: userId), token: token) { res in +// guard res.isConnected else { +// return +// } +// +// Client.shared.message(withId: messageId) { [weak self] res in +// if let message = res.value?.message, +// let channel = res.value?.channel { +// let messageWrapper = MessageWrapper(channel: channel, message: message) +// if let encodedData = try? JSONEncoder.stream.encode(messageWrapper), +// let encodedString = String(data: encodedData, encoding: .utf8) { +// let storedMessages = sharedDefaults.stringArray(forKey: "messageQueue") ?? [] +// sharedDefaults.setValue(storedMessages + [encodedString], forKey: "messageQueue") +// +// // Modify the notification content here... +// self?.bestAttemptContent?.title = "[modified] \(self?.bestAttemptContent?.title ?? "")" +// contentHandler(self?.bestAttemptContent ?? request.content) +// } +// Client.shared.disconnect() +// } +// } +// } } override func serviceExtensionTimeWillExpire() { @@ -58,125 +58,125 @@ final class NotificationService: UNNotificationServiceExtension { } } -public struct MessageWrapper: Encodable { - private enum CodingKeys: String, CodingKey { - case id - case channel - case type - case user - case created = "created_at" - case updated = "updated_at" - case text - case command - case args - case attachments - case parentId = "parent_id" - case showReplyInChannel = "show_in_channel" - case mentionedUsers = "mentioned_users" - } - - init(channel: Channel, message: Message) { - id = message.id - type = message.type - user = message.user - created = message.created - updated = message.updated - text = message.text - command = message.command - args = message.args - attachments = message.attachments - parentId = message.parentId - showReplyInChannel = message.showReplyInChannel - mentionedUsers = message.mentionedUsers - extraData = message.extraData - self.channel = ChannelWrapper(channel: channel) - } - - /// A message id. - public let id: String - /// The channel cid. - public let channel: ChannelWrapper? - /// A message type (see `MessageType`). - public let type: MessageType - /// A user (see `User`). - public let user: User - /// A created date. - public let created: Date - /// A updated date. - public let updated: Date - /// A text. - public let text: String - /// A used command name. - public let command: String? - /// A used command args. - public let args: String? - /// Attachments (see `Attachment`). - public let attachments: [Attachment] - /// A parent message id. - public let parentId: String? - /// Check if this reply message needs to show in the channel. - public let showReplyInChannel: Bool - /// Mentioned users (see `User`). - public let mentionedUsers: [User] - /// An extra data for the message. - public let extraData: Codable? -} - -public struct ChannelWrapper: Encodable { - /// Coding keys for the encoding. - private enum CodingKeys: String, CodingKey { - case id - case cid - case type - case name - case imageURL = "image" - case members - case lastMessageDate = "last_message_at" - case createdBy = "created_by" - case created = "created_at" - case deleted = "deleted_at" - case frozen - } - - init(channel: Channel) { - id = channel.id - cid = channel.cid - type = channel.type - name = channel.name - imageURL = channel.imageURL - lastMessageDate = channel.lastMessageDate - created = channel.created - deleted = channel.deleted - createdBy = channel.createdBy - config = channel.config - frozen = channel.frozen - extraData = channel.extraData - } - - /// A channel id. - public let id: String - /// A channel type + id. - public let cid: ChannelId - /// A channel type. - public let type: ChannelType - /// A channel name. - public let name: String? - /// An image of the channel. - public let imageURL: URL? - /// The last message date. - public let lastMessageDate: Date? - /// A channel created date. - public let created: Date - /// A channel deleted date. - public let deleted: Date? - /// A creator of the channel. - public let createdBy: User? - /// A config. - public let config: Channel.Config - /// Checks if the channel is frozen. - public let frozen: Bool - /// A list of user ids of the channel members. - public let members = Set() - /// An extra data for the channel. - public let extraData: Codable? -} +//public struct MessageWrapper: Encodable { +// private enum CodingKeys: String, CodingKey { +// case id +// case channel +// case type +// case user +// case created = "created_at" +// case updated = "updated_at" +// case text +// case command +// case args +// case attachments +// case parentId = "parent_id" +// case showReplyInChannel = "show_in_channel" +// case mentionedUsers = "mentioned_users" +// } +// +// init(channel: Channel, message: Message) { +// id = message.id +// type = message.type +// user = message.user +// created = message.created +// updated = message.updated +// text = message.text +// command = message.command +// args = message.args +// attachments = message.attachments +// parentId = message.parentId +// showReplyInChannel = message.showReplyInChannel +// mentionedUsers = message.mentionedUsers +// extraData = message.extraData +// self.channel = ChannelWrapper(channel: channel) +// } +// +// /// A message id. +// public let id: String +// /// The channel cid. +// public let channel: ChannelWrapper? +// /// A message type (see `MessageType`). +// public let type: MessageType +// /// A user (see `User`). +// public let user: User +// /// A created date. +// public let created: Date +// /// A updated date. +// public let updated: Date +// /// A text. +// public let text: String +// /// A used command name. +// public let command: String? +// /// A used command args. +// public let args: String? +// /// Attachments (see `Attachment`). +// public let attachments: [Attachment] +// /// A parent message id. +// public let parentId: String? +// /// Check if this reply message needs to show in the channel. +// public let showReplyInChannel: Bool +// /// Mentioned users (see `User`). +// public let mentionedUsers: [User] +// /// An extra data for the message. +// public let extraData: Codable? +//} +// +//public struct ChannelWrapper: Encodable { +// /// Coding keys for the encoding. +// private enum CodingKeys: String, CodingKey { +// case id +// case cid +// case type +// case name +// case imageURL = "image" +// case members +// case lastMessageDate = "last_message_at" +// case createdBy = "created_by" +// case created = "created_at" +// case deleted = "deleted_at" +// case frozen +// } +// +// init(channel: Channel) { +// id = channel.id +// cid = channel.cid +// type = channel.type +// name = channel.name +// imageURL = channel.imageURL +// lastMessageDate = channel.lastMessageDate +// created = channel.created +// deleted = channel.deleted +// createdBy = channel.createdBy +// config = channel.config +// frozen = channel.frozen +// extraData = channel.extraData +// } +// +// /// A channel id. +// public let id: String +// /// A channel type + id. +// public let cid: ChannelId +// /// A channel type. +// public let type: ChannelType +// /// A channel name. +// public let name: String? +// /// An image of the channel. +// public let imageURL: URL? +// /// The last message date. +// public let lastMessageDate: Date? +// /// A channel created date. +// public let created: Date +// /// A channel deleted date. +// public let deleted: Date? +// /// A creator of the channel. +// public let createdBy: User? +// /// A config. +// public let config: Channel.Config +// /// Checks if the channel is frozen. +// public let frozen: Bool +// /// A list of user ids of the channel members. +// public let members = Set() +// /// An extra data for the channel. +// public let extraData: Codable? +//} diff --git a/example/ios/Podfile b/example/ios/Podfile index 9e328189..63861e27 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -10,81 +10,34 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - generated_key_values + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + target 'Runner' do use_frameworks! use_modular_headers! - - # Flutter Pod - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end - end - - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' - pod 'StreamChatClient', '~> 2.0.0' - # Plugin Pods - - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true +pod 'StreamChatClient' post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index de801ca0..28b42300 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -33,53 +33,49 @@ PODS: - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter - - Firebase/Core (6.28.1): + - Firebase/CoreOnly (6.26.0): + - FirebaseCore (= 6.7.2) + - Firebase/Messaging (6.26.0): - Firebase/CoreOnly - - FirebaseAnalytics (= 6.6.2) - - Firebase/CoreOnly (6.28.1): - - FirebaseCore (= 6.9.1) - - Firebase/Messaging (6.28.1): - - Firebase/CoreOnly - - FirebaseMessaging (~> 4.6.0) - - firebase_messaging (0.0.1): - - Firebase/Core - - Firebase/Messaging + - FirebaseMessaging (~> 4.4.1) + - firebase_core (0.5.0): + - Firebase/CoreOnly (~> 6.26.0) - Flutter - - FirebaseAnalytics (6.6.2): - - FirebaseCore (~> 6.8) - - FirebaseInstallations (~> 1.4) - - GoogleAppMeasurement (= 6.6.2) - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (~> 1.30905.0) - - FirebaseCore (6.9.1): + - firebase_messaging (7.0.2): + - Firebase/CoreOnly (~> 6.26.0) + - Firebase/Messaging (~> 6.26.0) + - firebase_core + - Flutter + - FirebaseAnalyticsInterop (1.5.0) + - FirebaseCore (6.7.2): - FirebaseCoreDiagnostics (~> 1.3) + - FirebaseCoreDiagnosticsInterop (~> 1.2) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/Logger (~> 6.5) + - FirebaseCoreDiagnostics (1.7.0): + - GoogleDataTransport (~> 7.4) - GoogleUtilities/Environment (~> 6.7) - GoogleUtilities/Logger (~> 6.7) - - FirebaseCoreDiagnostics (1.5.0): - - GoogleDataTransport (~> 7.0) - - GoogleUtilities/Environment (~> 6.7) - - GoogleUtilities/Logger (~> 6.7) - - nanopb (~> 1.30905.0) - - FirebaseInstallations (1.5.0): - - FirebaseCore (~> 6.8) - - GoogleUtilities/Environment (~> 6.7) - - GoogleUtilities/UserDefaults (~> 6.7) + - nanopb (~> 1.30906.0) + - FirebaseCoreDiagnosticsInterop (1.2.0) + - FirebaseInstallations (1.3.0): + - FirebaseCore (~> 6.6) + - GoogleUtilities/Environment (~> 6.6) + - GoogleUtilities/UserDefaults (~> 6.6) - PromisesObjC (~> 1.2) - - FirebaseInstanceID (4.5.0): - - FirebaseCore (~> 6.8) + - FirebaseInstanceID (4.3.4): + - FirebaseCore (~> 6.6) - FirebaseInstallations (~> 1.0) - - GoogleUtilities/Environment (~> 6.7) - - GoogleUtilities/UserDefaults (~> 6.7) - - FirebaseMessaging (4.6.0): - - FirebaseCore (~> 6.8) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/UserDefaults (~> 6.5) + - FirebaseMessaging (4.4.1): + - FirebaseAnalyticsInterop (~> 1.5) + - FirebaseCore (~> 6.6) - FirebaseInstanceID (~> 4.3) - - GoogleUtilities/AppDelegateSwizzler (~> 6.7) - - GoogleUtilities/Environment (~> 6.7) - - GoogleUtilities/Reachability (~> 6.7) - - GoogleUtilities/UserDefaults (~> 6.7) + - GoogleUtilities/AppDelegateSwizzler (~> 6.5) + - GoogleUtilities/Environment (~> 6.5) + - GoogleUtilities/Reachability (~> 6.5) + - GoogleUtilities/UserDefaults (~> 6.5) - Protobuf (>= 3.9.2, ~> 3.9) - Flutter (1.0.0) - flutter_apns (0.0.1): @@ -88,117 +84,92 @@ PODS: - Flutter - flutter_local_notifications (0.0.1): - Flutter - - flutter_plugin_android_lifecycle (0.0.1): - - Flutter - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - GoogleAppMeasurement (6.6.2): - - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - - GoogleUtilities/MethodSwizzler (~> 6.0) - - GoogleUtilities/Network (~> 6.0) - - "GoogleUtilities/NSData+zlib (~> 6.0)" - - nanopb (~> 1.30905.0) - - GoogleDataTransport (7.0.0): - - nanopb (~> 1.30905.0) - - GoogleUtilities/AppDelegateSwizzler (6.7.1): + - GoogleDataTransport (7.4.0): + - nanopb (~> 1.30906.0) + - GoogleUtilities/AppDelegateSwizzler (6.7.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (6.7.1): + - GoogleUtilities/Environment (6.7.2): - PromisesObjC (~> 1.2) - - GoogleUtilities/Logger (6.7.1): + - GoogleUtilities/Logger (6.7.2): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (6.7.1): - - GoogleUtilities/Logger - - GoogleUtilities/Network (6.7.1): + - GoogleUtilities/Network (6.7.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (6.7.1)" - - GoogleUtilities/Reachability (6.7.1): + - "GoogleUtilities/NSData+zlib (6.7.2)" + - GoogleUtilities/Reachability (6.7.2): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (6.7.1): + - GoogleUtilities/UserDefaults (6.7.2): - GoogleUtilities/Logger - - GzipSwift (5.1.1) - image_picker (0.0.1): - Flutter - - moor_ffi (0.0.1): - - Flutter - - nanopb (1.30905.0): - - nanopb/decode (= 1.30905.0) - - nanopb/encode (= 1.30905.0) - - nanopb/decode (1.30905.0) - - nanopb/encode (1.30905.0) + - nanopb (1.30906.0): + - nanopb/decode (= 1.30906.0) + - nanopb/encode (= 1.30906.0) + - nanopb/decode (1.30906.0) + - nanopb/encode (1.30906.0) - path_provider (0.0.1): - Flutter - - path_provider_linux (0.0.1): - - Flutter - - path_provider_macos (0.0.1): - - Flutter - - PromisesObjC (1.2.9) - - Protobuf (3.12.0) - - ReachabilitySwift (5.0.0) - - SDWebImage (5.8.4): - - SDWebImage/Core (= 5.8.4) - - SDWebImage/Core (5.8.4) + - PromisesObjC (1.2.10) + - Protobuf (3.13.0) + - SDWebImage (5.9.2): + - SDWebImage/Core (= 5.9.2) + - SDWebImage/Core (5.9.2) - shared_preferences (0.0.1): - Flutter - - shared_preferences_linux (0.0.1): - - Flutter - - shared_preferences_macos (0.0.1): - - Flutter - - shared_preferences_web (0.0.1): - - Flutter - sqflite (0.0.1): - Flutter - FMDB (~> 2.7.2) - - Starscream (3.1.1) - - StreamChatClient (2.0.1): - - GzipSwift (~> 5.1) - - ReachabilitySwift (~> 5.0) - - Starscream (~> 3.1) + - sqlite3 (3.32.3): + - sqlite3/common (= 3.32.3) + - sqlite3/common (3.32.3) + - sqlite3/fts5 (3.32.3): + - sqlite3/common + - sqlite3/json1 (3.32.3): + - sqlite3/common + - sqlite3/perf-threadsafe (3.32.3): + - sqlite3/common + - sqlite3/rtree (3.32.3): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - sqlite3 (~> 3.32.3) + - sqlite3/fts5 + - sqlite3/json1 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - Starscream (4.0.4) + - StreamChatClient (2.4.0): + - Starscream (~> 4.0) - SwiftyGif (5.3.0) - url_launcher (0.0.1): - Flutter - - url_launcher_linux (0.0.1): - - Flutter - - url_launcher_macos (0.0.1): - - Flutter - - url_launcher_web (0.0.1): - - Flutter - video_player (0.0.1): - Flutter - - video_player_web (0.0.1): - - Flutter - wakelock (0.0.1): - Flutter DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_apns (from `.symlinks/plugins/flutter_apns/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - - moor_ffi (from `.symlinks/plugins/moor_ffi/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - - path_provider_linux (from `.symlinks/plugins/path_provider_linux/ios`) - - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - - shared_preferences_linux (from `.symlinks/plugins/shared_preferences_linux/ios`) - - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) - - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - - StreamChatClient (~> 2.0.0) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - StreamChatClient - url_launcher (from `.symlinks/plugins/url_launcher/ios`) - - url_launcher_linux (from `.symlinks/plugins/url_launcher_linux/ios`) - - url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`) - - url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`) - video_player (from `.symlinks/plugins/video_player/ios`) - - video_player_web (from `.symlinks/plugins/video_player_web/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) SPEC REPOS: @@ -206,22 +177,21 @@ SPEC REPOS: - DKImagePickerController - DKPhotoGallery - Firebase - - FirebaseAnalytics + - FirebaseAnalyticsInterop - FirebaseCore - FirebaseCoreDiagnostics + - FirebaseCoreDiagnosticsInterop - FirebaseInstallations - FirebaseInstanceID - FirebaseMessaging - FMDB - - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities - - GzipSwift - nanopb - PromisesObjC - Protobuf - - ReachabilitySwift - SDWebImage + - sqlite3 - Starscream - StreamChatClient - SwiftyGif @@ -229,6 +199,8 @@ SPEC REPOS: EXTERNAL SOURCES: file_picker: :path: ".symlinks/plugins/file_picker/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: @@ -239,40 +211,20 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" - flutter_plugin_android_lifecycle: - :path: ".symlinks/plugins/flutter_plugin_android_lifecycle/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" - moor_ffi: - :path: ".symlinks/plugins/moor_ffi/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" - path_provider_linux: - :path: ".symlinks/plugins/path_provider_linux/ios" - path_provider_macos: - :path: ".symlinks/plugins/path_provider_macos/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" - shared_preferences_linux: - :path: ".symlinks/plugins/shared_preferences_linux/ios" - shared_preferences_macos: - :path: ".symlinks/plugins/shared_preferences_macos/ios" - shared_preferences_web: - :path: ".symlinks/plugins/shared_preferences_web/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" url_launcher: :path: ".symlinks/plugins/url_launcher/ios" - url_launcher_linux: - :path: ".symlinks/plugins/url_launcher_linux/ios" - url_launcher_macos: - :path: ".symlinks/plugins/url_launcher_macos/ios" - url_launcher_web: - :path: ".symlinks/plugins/url_launcher_web/ios" video_player: :path: ".symlinks/plugins/video_player/ios" - video_player_web: - :path: ".symlinks/plugins/video_player_web/ios" wakelock: :path: ".symlinks/plugins/wakelock/ios" @@ -280,50 +232,40 @@ SPEC CHECKSUMS: DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 - Firebase: ed042590caa0029392257529a8003c25ee82bc18 - firebase_messaging: 21344b3b3a7d9d325d63a70e3750c0c798fe1e03 - FirebaseAnalytics: 5fa308e1b13f838d0f6dc74719ac2a72e8c5afc4 - FirebaseCore: 687b8e6a0a4337b898a6326d68254c2f80c143af - FirebaseCoreDiagnostics: 7535fe695737f8c5b350584292a70b7f8ff0357b - FirebaseInstallations: 3c520c951305cbf9ca54eb891ff9e6d1fd384881 - FirebaseInstanceID: 358d5cb393d2750a745569ede06827c35aea530b - FirebaseMessaging: bdd4d573eab37ebee29bad4e7c4b0ef18fa1a952 + Firebase: 7cf5f9c67f03cb3b606d1d6535286e1080e57eb6 + firebase_core: 3134fe79d257d430f163b558caf52a10a87efe8a + firebase_messaging: 2844c37f9ce87c0904b38fe435223161b1a71528 + FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae + FirebaseCore: f42e5e5f382cdcf6b617ed737bf6c871a6947b17 + FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 + FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 + FirebaseInstallations: 6f5f680e65dc374397a483c32d1799ba822a395b + FirebaseInstanceID: cef67c4967c7cecb56ea65d8acbb4834825c587b + FirebaseMessaging: 29543feb343b09546ab3aa04d008ee8595b43c44 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec flutter_apns: f516b118e423fe7c0a38771180549c4d6cb67c2f flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_local_notifications: 9e4738ce2471c5af910d961a6b7eadcf57c50186 - flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleAppMeasurement: 8cd1f289d60e629cf16ab03363b9e89c776b9651 - GoogleDataTransport: 8a40cb194ad242b6f6dfe72c14fe40fc67c4dcd7 - GoogleUtilities: e121a3867449ce16b0e35ddf1797ea7a389ffdf2 - GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa + GoogleDataTransport: b7f406340a291370045a270c599e53c6fa6ec20f + GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 - moor_ffi: d66c9470c18e9cb333423bbcb493c105c6c774c6 - nanopb: c43f40fadfe79e8b8db116583945847910cbabc9 + nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c - path_provider_linux: 4d630dc393e1f20364f3e3b4a2ff41d9674a84e4 - path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 - PromisesObjC: b48e0338dbbac2207e611750777895f7a5811b75 - Protobuf: 2793fcd0622a00b546c60e7cbbcc493e043e9bb9 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - SDWebImage: cf6922231e95550934da2ada0f20f2becf2ceba9 + PromisesObjC: b14b1c6b68e306650688599de8a45e49fae81151 + Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 + SDWebImage: 0b42b8719ab0c5257177d5894306e8a336b21cbb shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d - shared_preferences_linux: afefbfe8d921e207f01ede8b60373d9e3b566b78 - shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 - shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 - Starscream: 4bb2f9942274833f7b4d296a55504dcfc7edb7b0 - StreamChatClient: 91b0f585e7dc92ade58e657daffafb16d485b2a6 + sqlite3: 8f7d2078ae27778699a622a94b853285793422a2 + sqlite3_flutter_libs: 5651f8ff48e3b44d910863c4ea5916085b1b245f + Starscream: 5178aed56b316f13fa3bc55694e583d35dd414d9 + StreamChatClient: 8c83a141e753e45fa096ff56d4b782d59e46f251 SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef - url_launcher_linux: ac237cb7a8058736e4aae38bdbcc748a4b394cc0 - url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 - url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e - video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7 wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4 -PODFILE CHECKSUM: aaaddd91b568e9a010ff3278737eb06bc017bd6e +PODFILE CHECKSUM: eb001256612a59f8f9e4d083ad8b9671e69dd184 COCOAPODS: 1.8.4 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 5405840a..db80aaed 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -308,9 +308,60 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Starscream-framework/Starscream.framework", + "${BUILT_PRODUCTS_DIR}/StreamChatClient-framework/StreamChatClient.framework", + "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", + "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", + "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", + "${PODS_ROOT}/../Flutter/Flutter.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", + "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", + "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", + "${BUILT_PRODUCTS_DIR}/flutter_apns/flutter_apns.framework", + "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework", + "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", + "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", + "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", + "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", + "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", + "${BUILT_PRODUCTS_DIR}/video_player/video_player.framework", + "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Starscream.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StreamChatClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_apns.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -489,7 +540,6 @@ }; 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -573,7 +623,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -629,7 +678,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/example/lib/main.dart b/example/lib/main.dart index a8d4f813..0363d6d3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -110,6 +110,14 @@ class ChannelListPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) { + return CreateChannelPage(); + })); + }, + ), body: ChannelsBloc( child: ChannelListView( swipeToAction: true, @@ -204,3 +212,189 @@ class ThreadPage extends StatelessWidget { ); } } + +class CreateChannelPage extends StatefulWidget { + @override + _CreateChannelPageState createState() => _CreateChannelPageState(); +} + +class _CreateChannelPageState extends State { + final ScrollController _scrollController = ScrollController(); + Client client; + List users = []; + List selectedUsers = []; + int offset = 0; + bool loading = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + title: Text( + 'Create a channel', + style: Theme.of(context).textTheme.headline6, + ), + ), + floatingActionButton: + selectedUsers.isNotEmpty ? _buildFAB(context) : SizedBox(), + body: _buildListView(), + ); + } + + ListView _buildListView() { + return ListView.builder( + controller: _scrollController, + itemBuilder: _itemBuilder, + itemCount: users.length, + ); + } + + Widget _itemBuilder(context, i) { + final user = users[i]; + return ListTile( + onLongPress: () { + _selectUser(user); + }, + selected: selectedUsers.contains(user), + onTap: () { + if (selectedUsers.isNotEmpty) { + return _selectUser(user); + } + _createChannel(context, [user]); + }, + leading: UserAvatar( + user: user, + ), + title: Text(user.name), + ); + } + + Widget _buildFAB(BuildContext context) { + return FloatingActionButton( + child: Icon(Icons.done), + onPressed: () async { + String name; + if (selectedUsers.length > 1) { + name = await _showEnterNameDialog(context); + if (name?.isNotEmpty != true) { + return; + } + } + + _createChannel(context, selectedUsers, name); + }, + ); + } + + Future _showEnterNameDialog(BuildContext context) { + final controller = TextEditingController(); + return showDialog( + context: context, + builder: (context) => SimpleDialog( + contentPadding: const EdgeInsets.all(16), + title: Text('Enter a name for the channel'), + children: [ + TextField( + controller: controller, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + ), + ButtonBar( + children: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'), + ), + FlatButton( + onPressed: () => Navigator.pop(context, controller.text), + child: Text('Ok'), + ), + ], + ), + ], + ), + ); + } + + Future _createChannel( + BuildContext context, + List users, [ + String name, + ]) async { + final channel = client.channel('messaging', extraData: { + 'members': [ + client.state.user.id, + ...users.map((e) => e.id), + ], + if (name != null) 'name': name, + }); + await channel.watch(); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + } + + void _selectUser(User user) { + if (!selectedUsers.contains(user)) { + setState(() { + selectedUsers.add(user); + }); + } else { + setState(() { + selectedUsers.remove(user); + }); + } + } + + @override + void initState() { + super.initState(); + + client = StreamChat.of(context).client; + + _scrollController.addListener(() async { + if (!loading && + _scrollController.offset >= + _scrollController.position.maxScrollExtent - 100) { + offset += 25; + await _queryUsers(); + } + }); + + _queryUsers(); + } + + Future _queryUsers() { + loading = true; + return client.queryUsers( + pagination: PaginationParams( + limit: 25, + offset: offset, + ), + sort: [ + SortOption( + 'name', + direction: SortOption.ASC, + ), + ], + ).then((value) { + setState(() { + users = [ + ...users, + ...value.users, + ]; + }); + }).whenComplete(() => loading = false); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7a09df86..3201f649 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,7 +1,7 @@ name: example description: A new Flutter project. -version: 1.0.13+15 +version: 1.0.14+16 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index f18f7a0a..c3d76202 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -78,7 +78,7 @@ class ChannelImage extends StatelessWidget { String image; if (snapshot.data?.containsKey('image') == true) { image = snapshot.data['image']; - } else if (channel.state.members.length == 2) { + } else if (channel.state.members?.length == 2) { final otherMember = channel.state.members .firstWhere((member) => member.user.id != streamChat.user.id); return StreamBuilder( diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index c3f31e56..eaa44398 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -66,6 +66,7 @@ class ChannelListView extends StatefulWidget { this.errorBuilder, this.onImageTap, this.swipeToAction = false, + this.pullToRefresh = true, }) : super(key: key); /// The builder that will be used in case of error @@ -114,6 +115,9 @@ class ChannelListView extends StatefulWidget { /// The function called when the image is tapped final Function(Channel) onImageTap; + /// Set it to false to disable the pull-to-refresh widget + final bool pullToRefresh; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -124,105 +128,146 @@ class _ChannelListViewState extends State @override Widget build(BuildContext context) { - final channelsProvider = ChannelsBloc.of(context); + final channelsBloc = ChannelsBloc.of(context); + + if (!widget.pullToRefresh) { + return _buildListView(channelsBloc); + } return RefreshIndicator( onRefresh: () async { - return channelsProvider.queryChannels( + return channelsBloc.queryChannels( filter: widget.filter, sortOptions: widget.sort, paginationParams: widget.pagination, options: widget.options, ); }, - child: StreamBuilder>( - stream: channelsProvider.channelsStream, - builder: (context, snapshot) { - if (snapshot.hasError) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); - } + child: _buildListView(channelsBloc), + ); + } - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); - } + StreamBuilder> _buildListView( + ChannelsBlocState channelsBlocState, + ) { + return StreamBuilder>( + stream: channelsBlocState.channelsStream, + builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } + if (widget.errorBuilder != null) { + return widget.errorBuilder(snapshot.error); + } + + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - right: 2.0, - ), - child: Icon(Icons.error_outline), + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only( + right: 2.0, ), + child: Icon(Icons.error_outline), ), - TextSpan(text: 'Error loading channels'), - ], - ), - style: Theme.of(context).textTheme.headline6, + ), + TextSpan(text: 'Error loading channels'), + ], ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), - child: Text(message), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, ), - FlatButton( - onPressed: () { - channelsProvider.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - }, - child: Text('Retry'), - ), - ], - ), - ); - } - - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - final channels = snapshot.data; - return ListView.custom( - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); - }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, + child: Text(message), + ), + FlatButton( + onPressed: () { + channelsBlocState.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + }, + child: Text('Retry'), + ), + ], ), ); - }), - ); + } + + if (!snapshot.hasData) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, + ); + } + + final channels = snapshot.data; + + if (channels.isEmpty) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text('You have no channels currently'), + ), + ), + ); + }, + ); + } + + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channels); + }, + childCount: (channels.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channels.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + }); } Widget _itemBuilder(context, int i, List channels) { @@ -430,7 +475,7 @@ class _ChannelListViewState extends State filter: widget.filter, sortOptions: widget.sort, paginationParams: widget.pagination.copyWith( - offset: channelsProvider.channels.length, + offset: channelsProvider.channels?.length ?? 0, ), options: widget.options, ); @@ -482,7 +527,7 @@ class _ChannelListViewState extends State void didUpdateWidget(ChannelListView oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.filter != oldWidget.filter || + if (widget.filter?.toString() != oldWidget.filter?.toString() || widget.sort != oldWidget.sort || widget.pagination != oldWidget.pagination || widget.options != oldWidget.options) { diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index e2cf9cd9..df6f641b 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -11,7 +11,7 @@ class ChannelsBloc extends StatefulWidget { /// The widget child final Widget child; - /// Set this to false to prevent channels to be brought to the top of the list when a new message arrives + /// Set this to true to prevent channels to be brought to the top of the list when a new message arrives final bool lockChannelsOrder; /// Instantiate a new ChannelsBloc @@ -56,8 +56,7 @@ class ChannelsBlocState extends State final BehaviorSubject _queryChannelsLoadingController = BehaviorSubject.seeded(false); - final BehaviorSubject> _channelsController = - BehaviorSubject.seeded([]); + final BehaviorSubject> _channelsController = BehaviorSubject(); /// The stream notifying the state of queryChannel call Stream get queryChannelsLoading => @@ -73,7 +72,10 @@ class ChannelsBlocState extends State Map options, bool onlyOffline = false, }) async { - if (_queryChannelsLoadingController.value == true) { + final client = StreamChat.of(context).client; + + if (client.state?.user == null || + _queryChannelsLoadingController.value == true) { return; } _queryChannelsLoadingController.sink.add(true); @@ -82,16 +84,15 @@ class ChannelsBlocState extends State final clear = paginationParams == null || paginationParams.offset == null || paginationParams.offset == 0; - final oldChannels = List.from(channels); - StreamChat.of(context) - .client + final oldChannels = List.from(channels ?? []); + client .queryChannels( - filter: filter, - sort: sortOptions, - options: options, - paginationParams: paginationParams, - onlyOffline: onlyOffline, - ) + filter: filter, + sort: sortOptions, + options: options, + paginationParams: paginationParams, + onlyOffline: onlyOffline, + ) .listen((channels) { if (clear) { _channelsController.add(channels); @@ -123,23 +124,28 @@ class ChannelsBlocState extends State _subscriptions.add(client.on(EventType.messageNew).listen((e) { final newChannels = List.from(channels ?? []); final index = newChannels.indexWhere((c) => c.cid == e.cid); - if (index > 0) { - final channel = newChannels.removeAt(index); - newChannels.insert(0, channel); - _channelsController.add(newChannels); + if (index > -1) { + if (index > 0) { + final channel = newChannels.removeAt(index); + newChannels.insert(0, channel); + } } else { final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid); if (hiddenIndex > -1) { newChannels.insert(0, _hiddenChannels[hiddenIndex]); _hiddenChannels.removeAt(hiddenIndex); - _channelsController.add(newChannels); + } else { + if (client.state.channels[e.cid] != null) { + newChannels.insert(0, client.state.channels[e.cid]); + } } } + _channelsController.add(newChannels); })); } _subscriptions.add(client.on(EventType.channelHidden).listen((event) async { - final newChannels = List.from(channels); + final newChannels = List.from(channels ?? []); final channelIndex = newChannels.indexWhere((c) => c.cid == event.cid); if (channelIndex > -1) { final channel = newChannels.removeAt(channelIndex); diff --git a/lib/src/full_screen_image.dart b/lib/src/full_screen_image.dart index cc6a244a..76953b1a 100644 --- a/lib/src/full_screen_image.dart +++ b/lib/src/full_screen_image.dart @@ -15,12 +15,20 @@ class FullScreenImage extends StatelessWidget { @override Widget build(BuildContext context) { - return PhotoView( - imageProvider: CachedNetworkImageProvider(url), - maxScale: PhotoViewComputedScale.covered, - minScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: url, + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + iconTheme: IconThemeData( + color: Colors.white, + ), + ), + body: PhotoView( + imageProvider: CachedNetworkImageProvider(url), + maxScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: url, + ), ), ); } diff --git a/lib/src/full_screen_video.dart b/lib/src/full_screen_video.dart index 46325b07..98729243 100644 --- a/lib/src/full_screen_video.dart +++ b/lib/src/full_screen_video.dart @@ -26,6 +26,12 @@ class _FullScreenVideoState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + iconTheme: IconThemeData( + color: Colors.white, + ), + ), body: Builder( key: _scaffoldKey, builder: (context) { diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6358e39e..edc21f96 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -182,7 +182,7 @@ class MessageInputState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Stack( - overflow: Overflow.visible, + clipBehavior: Clip.none, children: [ _buildBorder(context), Column( @@ -738,7 +738,10 @@ class MessageInputState extends State { } else if (fileType == DefaultAttachmentTypes.file) { type = FileType.any; } - file = await FilePicker.getFile(type: type); + final res = await FilePicker.platform.pickFiles(type: type); + if (res?.files?.isNotEmpty == true) { + file = File(res.files.first.path); + } } setState(() { @@ -909,9 +912,9 @@ class MessageInputState extends State { ); } - return sendingFuture.whenComplete(() { + return sendingFuture.then((resp) { if (widget.onMessageSent != null) { - widget.onMessageSent(message); + widget.onMessageSent(resp.message); } else { if (widget.editMessage != null) { Navigator.pop(context); diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 14c8e20b..49394aa4 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -104,6 +104,7 @@ class MessageListView extends StatefulWidget { this.onThreadTap, this.dateDividerBuilder, this.scrollPhysics = const AlwaysScrollableScrollPhysics(), + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, }) : super(key: key); /// Function used to build a custom message widget @@ -128,6 +129,9 @@ class MessageListView extends StatefulWidget { /// The ScrollPhysics used by the ListView final ScrollPhysics scrollPhysics; + /// The [ScrollViewKeyboardDismissBehavior] used by the ListView + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -159,6 +163,7 @@ class _MessageListViewState extends State { child: ListView.custom( key: Key('messageListView'), physics: widget.scrollPhysics, + keyboardDismissBehavior: widget.keyboardDismissBehavior, controller: _scrollController, reverse: true, childrenDelegate: SliverChildBuilderDelegate( diff --git a/lib/src/message_text.dart b/lib/src/message_text.dart index 6ca99f6c..2ee6a6f8 100644 --- a/lib/src/message_text.dart +++ b/lib/src/message_text.dart @@ -11,10 +11,12 @@ class MessageText extends StatelessWidget { @required this.message, @required this.messageTheme, this.onMentionTap, + this.onLinkTap, }) : super(key: key); final Message message; final void Function(User) onMentionTap; + final void Function(String) onLinkTap; final MessageTheme messageTheme; @override @@ -35,7 +37,11 @@ class MessageText extends StatelessWidget { print('tap on ${mentionedUser.name}'); } } else { - launchURL(context, link); + if (onLinkTap != null) { + onLinkTap(link); + } else { + launchURL(context, link); + } } }, styleSheet: MarkdownStyleSheet.fromTheme( diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 5fea9fa0..841a2b26 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -101,6 +101,9 @@ class MessageWidget extends StatefulWidget { /// The function called when tapping on UserAvatar final void Function(User) onUserAvatarTap; + /// The function called when tapping on a link + final void Function(String) onLinkTap; + final List readList; /// If true show the users username next to the timestamp of the message @@ -132,6 +135,7 @@ class MessageWidget extends StatefulWidget { this.showDeleteMessage = true, this.showEditMessage = true, this.onUserAvatarTap, + this.onLinkTap, this.onMessageActions, this.editMessageInputBuilder, this.textBuilder, @@ -814,6 +818,7 @@ class _MessageWidgetState extends State { return widget.textBuilder != null ? widget.textBuilder(context, widget.message) : MessageText( + onLinkTap: widget.onLinkTap, message: widget.message, onMentionTap: widget.onMentionTap, messageTheme: widget.messageTheme, diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index ad6ba47c..dfc6095b 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -176,55 +176,58 @@ class StreamChatState extends State with WidgetsBindingObserver { @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 (client.state?.user != null) { + 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], + 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, + ), ); - 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, () { + }); + _disconnectTimer = Timer(client.backgroundKeepAlive, () { + client.disconnect(); + }); + } else { 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(); + } + } 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(); + } } } } diff --git a/lib/src/typing_indicator.dart b/lib/src/typing_indicator.dart index 57427da6..69a16b98 100644 --- a/lib/src/typing_indicator.dart +++ b/lib/src/typing_indicator.dart @@ -34,7 +34,7 @@ class TypingIndicator extends StatelessWidget { builder: (context, snapshot) { return AnimatedSwitcher( duration: Duration(milliseconds: 300), - child: snapshot.data.isNotEmpty + child: snapshot.data?.isNotEmpty == true ? Align( key: Key('typings'), alignment: alignment, diff --git a/pubspec.yaml b/pubspec.yaml index 7012f360..accccade 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 0.2.4 +version: 0.2.8+3 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -20,11 +20,11 @@ dependencies: url_launcher: ^5.4.11 video_player: ^0.10.11+1 chewie: ^0.9.10 - file_picker: ^1.12.0 + file_picker: ^2.0.0 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.2.1 flutter_svg: ^0.18.0 - stream_chat: ^0.2.3+3 + stream_chat: ^0.2.5+2 mime: ^0.9.6+3 visibility_detector: ^0.1.5 line_awesome_icons: ^1.0.4+2 @@ -38,3 +38,6 @@ flutter: dev_dependencies: pedantic: ^1.9.0 + flutter_test: + sdk: flutter + mockito: ^4.1.1 diff --git a/test/src/channel_preview_test.dart b/test/src/channel_preview_test.dart new file mode 100644 index 00000000..f5e0fbd0 --- /dev/null +++ b/test/src/channel_preview_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class MockClient extends Mock implements Client {} + +class MockChannel extends Mock implements Channel {} + +class MockChannelState extends Mock implements ChannelClientState {} + +void main() { + testWidgets( + 'it should show basic channel information', + (WidgetTester tester) async { + final client = MockClient(); + final channel = MockChannel(); + final channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(channel.lastMessageAt).thenReturn(lastMessageAt); + when(channel.state).thenReturn(channelState); + when(channel.extraData).thenReturn({ + 'name': 'test name', + }); + when(channelState.unreadCount).thenReturn(1); + when(channelState.lastMessage).thenReturn(Message( + text: 'hello', + )); + + await tester.pumpWidget(MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelPreview( + channel: channel, + ), + ), + ), + ), + )); + + expect(find.text('22/06/2020'), findsOneWidget); + expect(find.text('test name'), findsOneWidget); + expect(find.text('1'), findsOneWidget); + expect(find.text('hello'), findsOneWidget); + expect(find.byType(ChannelImage), findsOneWidget); + }, + ); +}