diff --git a/.flutter-plugins b/.flutter-plugins new file mode 100644 index 00000000..25fdba30 --- /dev/null +++ b/.flutter-plugins @@ -0,0 +1,9 @@ +# This is a generated file; do not edit or check into version control. +path_provider=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.1/ +screen=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/screen-0.0.5/ +sqflite=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.2.1/ +url_launcher=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-5.4.2/ +url_launcher_macos=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.0.1+4/ +url_launcher_web=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-0.1.1+1/ +video_player=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.8+1/ +video_player_web=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.2+1/ diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies new file mode 100644 index 00000000..c8053162 --- /dev/null +++ b/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"path_provider","dependencies":[]},{"name":"screen","dependencies":[]},{"name":"sqflite","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_web","url_launcher_macos"]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}]} \ No newline at end of file diff --git a/.gitignore b/.gitignore index dbef116d..2ea557c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,42 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +coverage/ +coverage_helper_test.dart + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages # See https://www.dartlang.org/guides/libraries/private-files # Files and directories created by pub diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b137891..1de3bc7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ +## 0.0.1 +- First release \ No newline at end of file diff --git a/README.md b/README.md index 3fb35e2f..77454051 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ TODO ```yaml dependencies: - stream_chat_flutter: ^0.1.0 + stream_chat_flutter: ^0.0.1 ``` You should then run `flutter packages get` diff --git a/analysis_options.yaml b/analysis_options.yaml index 4b8dc35b..fedac90d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -39,12 +39,12 @@ linter: # - one_member_abstracts # - only_throw_errors # - overridden_fields - - package_api_docs +# - package_api_docs - package_names - package_prefixed_library_names - prefer_is_not_empty # - prefer_mixin # https://github.com/dart-lang/language/issues/32 - - public_member_api_docs +# - public_member_api_docs - slash_for_doc_comments # - sort_constructors_first # - sort_unnamed_constructors_first diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 00000000..ae1f1838 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,37 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 00000000..01d2dcb9 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..a1356260 --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 00000000..bc2100d8 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,7 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 00000000..0f6a5e54 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..c208884f --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bc6007d --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 00000000..1656503f --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,12 @@ +package com.example.example + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..00fa4417 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..c208884f --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 00000000..3100ad2d --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 00000000..38c8d454 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..296b146b --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 00000000..5a2f14fb --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 00000000..e96ef602 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..6b4c0f78 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..e8efba11 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..399e9340 --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 00000000..b30a428b --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,90 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + 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 + end + generated_key_values +end + +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' + + # 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 +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 + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..0d45b982 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,518 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..a28140cf --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 00000000..a060db61 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..7335fdf9 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/example/lib/multiple_conversation.dart b/example/lib/multiple_conversation.dart new file mode 100644 index 00000000..682f364f --- /dev/null +++ b/example/lib/multiple_conversation.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() async { + final client = Client('qk4nn7rpcn75'); + + await client.setUser( + User(id: "wild-breeze-7"), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', + ); + + runApp( + StreamChat( + client: client, + child: MaterialApp( + home: ChannelListPage(), + ), + ), + ); +} + +class ChannelListPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: ChannelListView( + filter: { + 'members': { + '\$in': [StreamChat.of(context).user.id], + } + }, + sort: [SortOption("last_message_at")], + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + ), + ); + } +} + +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); + } +} diff --git a/example/lib/single_conversation.dart b/example/lib/single_conversation.dart new file mode 100644 index 00000000..b0008765 --- /dev/null +++ b/example/lib/single_conversation.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() async { + final client = Client('qk4nn7rpcn75'); + + await client.setUser( + User(id: "wild-breeze-7"), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', + ); + + final channelClient = client.channel('messaging', id: 'godevs'); + + channelClient.watch(); + + runApp( + StreamChat( + client: client, + child: MaterialApp( + home: StreamChannel( + channelClient: channelClient, + child: ChannelPage(), + ), + ), + ), + ); +} + +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 00000000..35756fdf --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: example +description: A new Flutter project. + +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + stream_chat_flutter: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart new file mode 100644 index 00000000..f28db180 --- /dev/null +++ b/lib/src/channel_image.dart @@ -0,0 +1,25 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class ChannelImage extends StatelessWidget { + const ChannelImage({ + Key key, + @required this.channel, + }) : super(key: key); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + radius: 20, + backgroundImage: channel.extraData.containsKey('image') + ? CachedNetworkImageProvider(channel.extraData['image'] as String) + : null, + child: channel.extraData.containsKey('image') + ? null + : Text(channel.config.name[0]), + ); + } +} diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart new file mode 100644 index 00000000..94272044 --- /dev/null +++ b/lib/src/channel_list_view.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'channel_preview.dart'; +import 'stream_channel.dart'; +import 'stream_chat.dart'; + +typedef ChannelTapCallback = void Function(ChannelClient, Widget); + +class ChannelListView extends StatefulWidget { + ChannelListView({ + Key key, + this.filter, + this.options, + this.sort, + this.pagination, + this.onChannelTap, + this.channelWidget, + this.channelPreview, + }) : assert(channelWidget != null || onChannelTap != null), + super(key: key); + + final Map filter; + final Map options; + final List sort; + final PaginationParams pagination; + final ScrollController _scrollController = ScrollController(); + final ChannelTapCallback onChannelTap; + final Widget channelWidget; + final Widget channelPreview; + + @override + _ChannelListViewState createState() => _ChannelListViewState(); +} + +class _ChannelListViewState extends State { + @override + Widget build(BuildContext context) { + final streamChat = StreamChat.of(context); + return RefreshIndicator( + onRefresh: () async { + streamChat.clearChannels(); + return streamChat.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + }, + child: StreamBuilder>( + stream: streamChat.channelsStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + if (snapshot.hasError) { + print((snapshot.error as Error).stackTrace); + return Center( + child: Text(snapshot.error.toString()), + ); + } + + final channelsStates = snapshot.data; + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: widget._scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channelsStates); + }, + childCount: (channelsStates.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channelsStates.indexWhere( + (cs) => 'CHANNEL-${cs.channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + }), + ); + } + + Widget _itemBuilder(context, int i, List channelsStates) { + if (i % 2 != 0) { + return _separatorBuilder(context, i); + } + + i = i ~/ 2; + + final streamChat = StreamChat.of(context); + if (i < channelsStates.length) { + final channelState = channelsStates[i]; + + final channelClient = + streamChat.client.channelClients[channelState.channel.id]; + + ChannelTapCallback onTap; + if (widget.onChannelTap != null) { + onTap = widget.onChannelTap; + } else { + onTap = (client, _) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: widget.channelWidget, + channelClient: client, + ); + }, + ), + ); + }; + } + + Widget child; + if (widget.channelPreview != null) { + child = Stack( + children: [ + widget.channelPreview, + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + onTap(channelClient, widget.channelWidget); + }, + ), + ), + ), + ], + ); + } else { + child = ChannelPreview( + onTap: (channelClient) { + onTap(channelClient, widget.channelWidget); + }, + ); + } + + return StreamChannel( + key: ValueKey('CHANNEL-${channelClient.id}'), + child: child, + channelClient: channelClient, + ); + } else { + return _buildQueryProgressIndicator(context, streamChat); + } + } + + Widget _buildQueryProgressIndicator(context, StreamChat streamChat) { + return StreamBuilder( + stream: streamChat.queryChannelsLoading, + initialData: false, + builder: (context, snapshot) { + return Container( + height: 100, + padding: EdgeInsets.all(32), + child: Center( + child: snapshot.data ? CircularProgressIndicator() : Container(), + ), + ); + }); + } + + Widget _separatorBuilder(context, i) { + return Container( + height: 1, + color: Colors.black.withOpacity(0.1), + margin: EdgeInsets.symmetric(horizontal: 16), + ); + } + + void _listenChannelPagination(StreamChat streamChat) { + if (widget._scrollController.position.maxScrollExtent == + widget._scrollController.position.pixels) { + streamChat.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination.copyWith( + offset: streamChat.channels.length, + ), + options: widget.options, + ); + } + } + + @override + void initState() { + super.initState(); + + final streamChat = StreamChat.of(context); + streamChat.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + + widget._scrollController.addListener(() { + _listenChannelPagination(streamChat); + }); + } +} diff --git a/lib/src/channel_name_text.dart b/lib/src/channel_name_text.dart new file mode 100644 index 00000000..f31f38c5 --- /dev/null +++ b/lib/src/channel_name_text.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class ChannelNameText extends StatelessWidget { + const ChannelNameText({ + Key key, + this.channel, + }) : super(key: key); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return Text( + channel.extraData['name'] as String ?? channel.config.name, + style: Theme.of(context).textTheme.body2, + ); + } +} diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart new file mode 100644 index 00000000..e390a9f6 --- /dev/null +++ b/lib/src/channel_preview.dart @@ -0,0 +1,147 @@ +import 'package:date_format/date_format.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'channel_image.dart'; +import 'channel_name_text.dart'; +import 'stream_channel.dart'; +import 'stream_chat.dart'; + +class ChannelPreview extends StatelessWidget { + final void Function(ChannelClient) onTap; + + const ChannelPreview({ + Key key, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final streamChannel = StreamChannel.of(context); + return StreamBuilder( + stream: streamChannel.channelClient.state.channelStateStream, + initialData: streamChannel.channelState, + builder: (context, snapshot) { + final channelState = snapshot.data; + return _buildChannelPreview( + context, + channelState, + streamChannel, + ); + }); + } + + StreamChannel _buildChannelPreview( + BuildContext context, + ChannelState channelState, + StreamChannelState streamChannel, + ) { + final channelClient = + StreamChat.of(context).client.channelClients[channelState.channel.id]; + return StreamChannel( + channelClient: channelClient, + child: ListTile( + onTap: () { + onTap(channelClient); + }, + leading: ChannelImage( + channel: channelState.channel, + ), + title: ChannelNameText( + channel: channelState.channel, + ), + subtitle: _buildSubtitle( + streamChannel, + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildDate(context, channelState.channel.lastMessageAt), + ], + ), + ), + ); + } + + Text _buildDate(BuildContext context, DateTime lastMessageAt) { + String stringDate; + final now = DateTime.now(); + + if (now.year != lastMessageAt.year || + now.month != lastMessageAt.month || + now.day != lastMessageAt.day) { + stringDate = + '${lastMessageAt.day}/${lastMessageAt.month}/${lastMessageAt.year}'; + stringDate = formatDate(lastMessageAt, [dd, '/', mm, '/', yyyy]); + } else { + stringDate = '${lastMessageAt.hour}:${lastMessageAt.minute}'; + stringDate = formatDate(lastMessageAt, [HH, ':', nn]); + } + + return Text( + stringDate, + style: Theme.of(context).textTheme.caption, + ); + } + + Widget _buildSubtitle( + StreamChannelState streamChannel, + ) { + return StreamBuilder>( + stream: streamChannel.channelClient.state.typingEventsStream, + initialData: [], + builder: (context, snapshot) { + final typings = snapshot.data; + final opacity = + streamChannel.channelClient.state.unreadCount > .0 ? 1.0 : 0.5; + return typings.isNotEmpty + ? _buildTypings(typings, context, opacity) + : _buildLastMessage(context, streamChannel, opacity); + }); + } + + Widget _buildLastMessage( + BuildContext context, StreamChannelState streamChannel, double opacity) { + final lastMessage = streamChannel.channelState.messages.isNotEmpty + ? streamChannel.channelState.messages.last + : null; + if (lastMessage == null) { + return SizedBox.fromSize( + size: Size.zero, + ); + } + + final prefix = lastMessage.attachments + .map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } + return null; + }) + .where((e) => e != null) + .join(' '); + + return Text( + '$prefix ${lastMessage.text ?? ''}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.caption.copyWith( + color: Colors.black.withOpacity(opacity), + ), + ); + } + + Text _buildTypings(List typings, BuildContext context, double opacity) { + return Text( + '${typings.map((u) => u.extraData.containsKey('name') ? u.extraData['name'] : u.id).join(',')} ${typings.length == 1 ? 'is' : 'are'} typing...', + maxLines: 1, + style: Theme.of(context).textTheme.caption.copyWith( + color: Colors.black.withOpacity(opacity), + ), + ); + } +} diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart new file mode 100644 index 00000000..243f1d31 --- /dev/null +++ b/lib/src/message_input.dart @@ -0,0 +1,137 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'stream_channel.dart'; + +class MessageInput extends StatefulWidget { + MessageInput({ + Key key, + this.onMessageSent, + this.parent, + }) : super(key: key); + + final void Function(Message) onMessageSent; + final Message parent; + + @override + _MessageInputState createState() => _MessageInputState(); +} + +class _MessageInputState extends State { + final _textController = TextEditingController(); + bool _messageIsPresent = false; + bool _typingStarted = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: MediaQuery.of(context).size.width, + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: _typingStarted + ? LinearGradient(colors: [Color(0xFF00AEFF), Color(0xFF0076FF)]) + : null, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + borderRadius: BorderRadius.circular(10.0), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withAlpha(5), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black.withOpacity(.2)), + ), + child: Flex( + direction: Axis.horizontal, + children: [ + Expanded( + child: TextField( + minLines: null, + maxLines: null, + onSubmitted: (_) { + _sendMessage(context); + }, + controller: _textController, + onChanged: (s) { + StreamChannel.of(context).channelClient.keyStroke(); + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + }); + }, + onTap: () { + setState(() { + _typingStarted = true; + }); + }, + style: Theme.of(context).textTheme.body1, + autofocus: false, + decoration: InputDecoration( + hintText: 'Write a message', + prefixText: ' ', + border: InputBorder.none, + ), + ), + ), + AnimatedCrossFade( + crossFadeState: _messageIsPresent + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: _buildSendButton(context), + secondChild: Container(), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ], + ), + ), + ), + ), + ); + } + + IconButton _buildSendButton(BuildContext context) { + return IconButton( + onPressed: () { + _sendMessage(context); + }, + icon: Icon( + Icons.send, + ), + ); + } + + void _sendMessage(BuildContext context) { + final text = _textController.text.trim(); + if (text.isEmpty) { + return; + } + + _textController.clear(); + setState(() { + _messageIsPresent = false; + _typingStarted = false; + }); + FocusScope.of(context).unfocus(); + + StreamChannel.of(context) + .channelClient + .sendMessage( + Message( + parentId: widget.parent?.id, + text: text, + ), + ) + .then((_) { + if (widget.onMessageSent != null) { + widget.onMessageSent(Message(text: text)); + } + }); + } +} diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart new file mode 100644 index 00000000..86e7374b --- /dev/null +++ b/lib/src/message_list_view.dart @@ -0,0 +1,282 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_widgets/flutter_widgets.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'message_widget.dart'; +import 'stream_channel.dart'; + +typedef MessageBuilder = Widget Function(BuildContext, Message, int index); +typedef ParentMessageBuilder = Widget Function(BuildContext, Message); +typedef OnThreadSelectCallback = void Function(Message parent); + +class MessageListView extends StatefulWidget { + MessageListView({ + Key key, + MessageBuilder messageBuilder, + this.parentMessageBuilder, + this.parentMessage, + this.onThreadSelect, + }) : _messageBuilder = messageBuilder, + super(key: key); + + final MessageBuilder _messageBuilder; + final ParentMessageBuilder parentMessageBuilder; + final OnThreadSelectCallback onThreadSelect; + final Message parentMessage; + + @override + _MessageListViewState createState() => _MessageListViewState(); +} + +class _MessageListViewState extends State { + static const _newMessageLoadingOffset = 100; + final ScrollController _scrollController = ScrollController(); + bool _isBottom = true; + bool _topWasVisible = false; + List _messages = []; + List _newMessageList = []; + + @override + Widget build(BuildContext context) { + final streamChannel = StreamChannel.of(context); + + /// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed + return NotificationListener( + onNotification: (_) { + if (_scrollController.offset < 150 && _newMessageList.isNotEmpty) { + setState(() { + _messages.insertAll(0, _newMessageList); + _newMessageList.clear(); + }); + } + return true; + }, + child: ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + reverse: true, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + if (i == _messages.length + 1) { + if (widget.parentMessage != null) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, widget.parentMessage); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MessageWidget( + key: ValueKey( + 'PARENT-MESSAGE-${widget.parentMessage.id}'), + previousMessage: null, + message: widget.parentMessage.copyWith(replyCount: 0), + nextMessage: null, + onThreadSelect: widget.onThreadSelect, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of a new thread', + textAlign: TextAlign.center, + ), + color: Theme.of(context).primaryColorLight, + ), + ), + ], + ); + } + } else { + return SizedBox.fromSize( + size: Size.zero, + ); + } + } + + if (i == _messages.length) { + return _buildLoadingIndicator(streamChannel); + } + final message = _messages[i]; + + if (widget._messageBuilder != null) { + return widget._messageBuilder(context, message, i); + } + + final previousMessage = + i < _messages.length - 1 ? _messages[i + 1] : null; + final nextMessage = i > 0 ? _messages[i - 1] : null; + + if (i == 0) { + return _buildBottomMessage( + streamChannel, + previousMessage, + message, + context, + ); + } + + if (i == _messages.length - 1) { + return _buildTopMessage( + message, + nextMessage, + streamChannel, + context, + ); + } + + return MessageWidget( + key: ValueKey('MESSAGE-${message.id}'), + previousMessage: previousMessage, + message: message, + nextMessage: nextMessage, + onThreadSelect: widget.onThreadSelect, + ); + }, + childCount: _messages.length + 2, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = _messages + .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value); + return index != -1 ? index : null; + }, + ), + ), + ); + } + + Container _buildLoadingIndicator(StreamChannelState streamChannel) { + return Container( + height: 50, + child: StreamBuilder( + stream: streamChannel.queryMessage, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + print((snapshot.error as Error).stackTrace.toString()); + return Center( + child: Text(snapshot.error.toString()), + ); + } + if (!snapshot.data) { + return Container(); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), + ); + }), + ); + } + + Widget _buildTopMessage( + Message message, + Message nextMessage, + StreamChannelState streamChannelState, + BuildContext context, + ) { + return VisibilityDetector( + key: ValueKey('TOP-MESSAGE'), + child: MessageWidget( + key: ValueKey('MESSAGE-${message.id}'), + previousMessage: null, + message: message, + nextMessage: nextMessage, + onThreadSelect: widget.onThreadSelect, + ), + onVisibilityChanged: (visibility) { + final topIsVisible = visibility.visibleBounds != Rect.zero; + if (topIsVisible && !_topWasVisible) { + streamChannelState.queryMessages(); + } + _topWasVisible = topIsVisible; + }, + ); + } + + Widget _buildBottomMessage( + StreamChannelState channelBloc, + Message previousMessage, + Message message, + BuildContext context, + ) { + return VisibilityDetector( + key: ValueKey('BOTTOM-MESSAGE'), + onVisibilityChanged: (visibility) { + _isBottom = visibility.visibleBounds != Rect.zero; + if (_isBottom) { + if (channelBloc.channelClient.state.unreadCount > 0) { + channelBloc.channelClient.markRead(); + } + } + }, + child: MessageWidget( + key: ValueKey('MESSAGE-${message.id}'), + previousMessage: previousMessage, + message: message, + nextMessage: null, + onThreadSelect: widget.onThreadSelect, + ), + ); + } + + StreamSubscription _streamListener; + + @override + void initState() { + super.initState(); + + final streamChannel = StreamChannel.of(context); + if (streamChannel.channelClient.state.unreadCount > 0) { + streamChannel.channelClient.markRead(); + } + + Stream> stream; + + if (widget.parentMessage == null) { + stream = streamChannel.channelStateStream.map((c) => c.messages); + } else { + streamChannel.getReplies(widget.parentMessage.id); + stream = streamChannel.channelClient.state.threadsStream + .where((threads) => threads.containsKey(widget.parentMessage.id)) + .map((threads) => threads[widget.parentMessage.id]); + } + + _streamListener = stream.listen((newMessages) { + newMessages = newMessages.reversed.toList(); + if (_messages.isEmpty || newMessages.first.id != _messages.first.id) { + if (!_scrollController.hasClients || + _scrollController.offset < _newMessageLoadingOffset) { + setState(() { + _messages = newMessages; + }); + } else if (newMessages.first.user.id == + streamChannel.channelClient.client.state.user.id) { + _scrollController.jumpTo(0); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _messages = newMessages; + }); + }); + } else { + _newMessageList = newMessages; + } + } else { + setState(() { + _messages = newMessages; + }); + } + }); + } + + @override + void dispose() { + _streamListener.cancel(); + super.dispose(); + } +} diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart new file mode 100644 index 00000000..aba9ced1 --- /dev/null +++ b/lib/src/message_widget.dart @@ -0,0 +1,353 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chewie/chewie.dart'; +import 'package:date_format/date_format.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:video_player/video_player.dart'; + +import 'message_list_view.dart'; +import 'stream_chat.dart'; +import 'user_avatar.dart'; + +class MessageWidget extends StatefulWidget { + const MessageWidget({ + Key key, + @required this.previousMessage, + @required this.message, + @required this.nextMessage, + this.onThreadSelect, + }) : super(key: key); + + final Message previousMessage; + final Message message; + final Message nextMessage; + final OnThreadSelectCallback onThreadSelect; + + @override + _MessageWidgetState createState() => _MessageWidgetState(); +} + +class _MessageWidgetState extends State + with AutomaticKeepAliveClientMixin { + final Map _videoControllers = {}; + final Map _chuwieControllers = {}; + + @override + Widget build(BuildContext context) { + super.build(context); + + final streamChat = StreamChat.of(context); + final currentUserId = streamChat.user.id; + final messageUserId = widget.message.user.id; + final previousUserId = widget.previousMessage?.user?.id; + final nextUserId = widget.nextMessage?.user?.id; + final isMyMessage = messageUserId == currentUserId; + final isLastUser = previousUserId == messageUserId; + final isNextUser = nextUserId == messageUserId; + final alignment = + isMyMessage ? Alignment.centerRight : Alignment.centerLeft; + + var row = [ + Column( + crossAxisAlignment: + isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + _buildBubble(context, isMyMessage, isLastUser), + widget.message.replyCount > 0 + ? GestureDetector( + onTap: () { + if (widget.onThreadSelect != null) { + widget.onThreadSelect(widget.message); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + 'Replies: ${widget.message.replyCount}', + style: Theme.of(context) + .textTheme + .subtitle + .copyWith(color: Colors.blue), + ), + Icon( + Icons.subdirectory_arrow_left, + color: Colors.black12, + ), + ], + ), + ), + ) + : Container(), + isNextUser ? Container() : _buildTimestamp(isMyMessage, alignment), + ], + ), + isNextUser + ? Container( + width: 40, + ) + : Padding( + padding: EdgeInsets.only( + left: isMyMessage ? 8.0 : 0, + right: isMyMessage ? 0 : 8.0, + ), + child: UserAvatar(user: widget.message.user), + ), + ]; + + if (!isMyMessage) { + row = row.reversed.toList(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + margin: EdgeInsets.only( + top: isLastUser ? 5 : 24, + bottom: widget.nextMessage == null ? 30 : 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: + isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: row, + ), + ); + } + + Widget _buildBubble( + BuildContext context, + bool isMyMessage, + bool isLastUser, + ) { + var nOfAttachmentWidgets = 0; + + final column = Column( + crossAxisAlignment: + isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: widget.message.attachments.map((attachment) { + nOfAttachmentWidgets++; + + Widget attachmentWidget; + if (attachment.type == 'video') { + attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser); + } else if (attachment.type == 'image' || attachment.type == 'giphy') { + attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment); + } + + if (attachmentWidget != null) { + final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser) + .copyWith(color: Color(0xffebebeb)); + return ClipRRect( + borderRadius: boxDecoration.borderRadius, + child: Container( + decoration: boxDecoration, + constraints: BoxConstraints.loose(Size.fromWidth(300)), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + attachmentWidget, + attachment.title != null + ? Container( + constraints: + BoxConstraints.loose(Size.fromHeight(70)), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + attachment.title, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .subtitle + .copyWith(color: Colors.blue), + ), + Text( + Uri.parse(attachment.thumbUrl) + .authority + .split('.') + .reversed + .take(2) + .toList() + .reversed + .join('.'), + overflow: TextOverflow.ellipsis, + style: + Theme.of(context).textTheme.caption, + ), + ], + ), + ), + color: Color(0xffebebeb), + ) + : Container(), + ], + ), + attachment.type == 'image' && attachment.titleLink != null + ? Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _launchURL(attachment.titleLink), + ), + ), + ) + : SizedBox.fromSize( + size: Size.zero, + ), + ], + ), + margin: EdgeInsets.only( + top: nOfAttachmentWidgets > 1 ? 5 : 0, + ), + ), + ); + } + + nOfAttachmentWidgets--; + return Container(); + }).toList(), + ); + + if (widget.message.text.trim().isNotEmpty) { + column.children.add(Container( + margin: EdgeInsets.only( + top: nOfAttachmentWidgets > 0 ? 5 : 0, + ), + decoration: _buildBoxDecoration( + isMyMessage, isLastUser || nOfAttachmentWidgets > 0), + padding: EdgeInsets.all(10), + constraints: BoxConstraints.loose(Size.fromWidth(300)), + child: MarkdownBody( + data: '${widget.message.text}', + onTapLink: (link) { + _launchURL(link); + }, + styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)), + ), + )); + } + + return column; + } + + Widget _buildImage( + bool isMyMessage, + bool isLastUser, + Attachment attachment, + ) { + return CachedNetworkImage( + imageUrl: attachment.thumbUrl ?? attachment.imageUrl, + fit: BoxFit.cover, + ); + } + + Widget _buildVideo( + Attachment attachment, + bool isMyMessage, + bool isLastUser, + ) { + VideoPlayerController videoController; + if (_videoControllers.containsKey(attachment.assetUrl)) { + videoController = _videoControllers[attachment.assetUrl]; + } else { + videoController = VideoPlayerController.network(attachment.assetUrl); + _videoControllers[attachment.assetUrl] = videoController; + } + + ChewieController chewieController; + if (_chuwieControllers.containsKey(attachment.assetUrl)) { + chewieController = _chuwieControllers[attachment.assetUrl]; + } else { + chewieController = ChewieController( + videoPlayerController: videoController, + autoInitialize: true, + errorBuilder: (_, e) { + return Stack( + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: CachedNetworkImageProvider( + attachment.thumbUrl, + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _launchURL(attachment.titleLink), + ), + ), + ], + ); + }); + _chuwieControllers[attachment.assetUrl] = chewieController; + } + + return Chewie( + key: ValueKey( + 'ATTACHMENT-${attachment.title}-${widget.message.id}'), + controller: chewieController, + ); + } + + Future _launchURL(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text('Cannot launch the url'), + ), + ); + } + } + + @override + void dispose() { + _videoControllers.values.forEach((element) { + element.dispose(); + }); + super.dispose(); + } + + Widget _buildTimestamp(bool isMyMessage, Alignment alignment) { + return Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text( + formatDate(widget.message.createdAt.toLocal(), [HH, ':', nn]), + ), + ); + } + + BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) { + return BoxDecoration( + border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)), + borderRadius: BorderRadius.only( + topLeft: Radius.circular((isMyMessage || !isLastUser) ? 16 : 2), + bottomLeft: Radius.circular(isMyMessage ? 16 : 2), + topRight: Radius.circular((isMyMessage && isLastUser) ? 2 : 16), + bottomRight: Radius.circular(isMyMessage ? 2 : 16), + ), + color: isMyMessage ? Color(0xffebebeb) : Colors.white, + ); + } + + @override + bool get wantKeepAlive { + return widget.message.attachments.isNotEmpty; + } +} diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart new file mode 100644 index 00000000..fe34ff00 --- /dev/null +++ b/lib/src/stream_channel.dart @@ -0,0 +1,128 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class StreamChannel extends StatefulWidget { + StreamChannel({ + Key key, + @required this.child, + @required this.channelClient, + }) : super( + key: key, + ); + + final Widget child; + final ChannelClient channelClient; + + static StreamChannelState of(BuildContext context) { + StreamChannelState streamChannelState; + + streamChannelState = context.findAncestorStateOfType(); + + if (streamChannelState == null) { + throw Exception( + 'You must have a StreamChannel widget at the top of your widget tree'); + } + + return streamChannelState; + } + + @override + StreamChannelState createState() => StreamChannelState(); +} + +class StreamChannelState extends State { + StreamChannelState(); + + ChannelClient get channelClient => widget.channelClient; + + ChannelState get channelState => widget.channelClient.state.channelState; + + Stream get channelStateStream => + widget.channelClient.state.channelStateStream; + + final BehaviorSubject _queryMessageController = BehaviorSubject(); + + Stream get queryMessage => _queryMessageController.stream; + + void queryMessages() { + _queryMessageController.add(true); + + String firstId; + if (channelState.messages.isNotEmpty) { + firstId = channelState.messages.first.id; + } + + widget.channelClient + .query( + messagesPagination: PaginationParams( + lessThan: firstId, + limit: 100, + ), + ) + .then((res) { + _queryMessageController.add(false); + }).catchError((e, stack) { + _queryMessageController.addError(e, stack); + }); + } + + Future getReplies(String parentId) async { + _queryMessageController.add(true); + + String firstId; + if (widget.channelClient.state.threads.containsKey(parentId)) { + firstId = widget.channelClient.state.threads[parentId].first.id; + } + + return widget.channelClient + .getReplies( + parentId, + PaginationParams( + lessThan: firstId, + limit: 100, + ), + ) + .then((res) { + _queryMessageController.add(false); + }).catchError((e, stack) { + _queryMessageController.addError(e, stack); + }); + } + + @override + void dispose() { + _queryMessageController.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.channelClient == null) { + return Center( + child: CircularProgressIndicator(), + ); + } + return FutureBuilder( + future: widget.channelClient.initialized, + initialData: widget.channelClient.state != null, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (snapshot.hasError) { + return Center( + child: Text(snapshot.error), + ); + } else { + return widget.child; + } + }, + ); + } +} diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart new file mode 100644 index 00000000..79d447e3 --- /dev/null +++ b/lib/src/stream_chat.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class StreamChat extends InheritedWidget { + final Client client; + final List _subscriptions = []; + + StreamChat({ + Key key, + @required this.client, + @required Widget child, + }) : super( + key: key, + child: child, + ) { + _subscriptions.add(client.on('message.new').listen((Event e) { + final index = channels.indexWhere((c) => c.channel.cid == e.cid); + if (index > 0) { + final channel = channels.removeAt(index); + channels.insert(0, channel); + _channelsController.add(channels); + } + })); + } + + User get user => client.state.user; + + Stream get userStream => client.state.userStream; + + Stream> get channelsStream => _channelsController.stream; + final BehaviorSubject> _channelsController = + BehaviorSubject(); + final List channels = []; + + final BehaviorSubject _queryChannelsLoadingController = + BehaviorSubject.seeded(false); + + Stream get queryChannelsLoading => + _queryChannelsLoadingController.stream; + + Future queryChannels({ + Map filter, + List sortOptions, + PaginationParams paginationParams, + Map options, + }) async { + if (_queryChannelsLoadingController.value) { + return; + } + _queryChannelsLoadingController.sink.add(true); + + try { + final res = await client.queryChannels( + filter: filter, + sort: sortOptions, + options: options, + paginationParams: paginationParams, + ); + channels.addAll(res.map((c) => c.state.channelState)); + _channelsController.sink.add(channels); + _queryChannelsLoadingController.sink.add(false); + } catch (e) { + _channelsController.sink.addError(e); + } + } + + void clearChannels() { + channels.clear(); + } + + void dispose() { + client.dispose(); + _subscriptions.forEach((s) => s.cancel()); + _queryChannelsLoadingController.close(); + _channelsController.close(); + } + + @override + bool updateShouldNotify(InheritedWidget oldWidget) { + return true; + } + + static StreamChat of(BuildContext context, [bool listen = false]) { + StreamChat streamChat; + + if (listen) { + streamChat = context.dependOnInheritedWidgetOfExactType(); + } else { + streamChat = context.findAncestorWidgetOfExactType(); + } + + if (streamChat == null) { + throw Exception( + 'You must have a StreamChat widget at the top of your widget tree'); + } + + return streamChat; + } +} diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart new file mode 100644 index 00000000..6f7d0954 --- /dev/null +++ b/lib/src/user_avatar.dart @@ -0,0 +1,29 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class UserAvatar extends StatelessWidget { + const UserAvatar({ + Key key, + @required this.user, + this.radius = 16, + }) : super(key: key); + + final User user; + final double radius; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + radius: radius, + backgroundImage: user.extraData.containsKey('image') + ? CachedNetworkImageProvider(user.extraData['image'] as String) + : null, + child: user.extraData.containsKey('image') + ? null + : Text(user?.extraData?.containsKey('name') ?? false + ? user.extraData['name'][0] + : ''), + ); + } +} diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart new file mode 100644 index 00000000..37db4814 --- /dev/null +++ b/lib/stream_chat_flutter.dart @@ -0,0 +1,10 @@ +export 'package:stream_chat/stream_chat.dart'; + +export 'src/channel_list_view.dart'; +export 'src/channel_preview.dart'; +export 'src/message_input.dart'; +export 'src/message_list_view.dart'; +export 'src/message_widget.dart'; +export 'src/stream_channel.dart'; +export 'src/stream_chat.dart'; +export 'src/user_avatar.dart'; diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..32cf32e8 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,25 @@ +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.0.1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + rxdart: ^0.23.1 + flutter_widgets: ^0.1.10 + timeago: ^2.0.26 + date_format: ^1.0.8 + cached_network_image: ^2.0.0 + flutter_markdown: ^0.3.3 + url_launcher: ^5.4.1 + video_player: ^0.10.7 + chewie: ^0.9.8+1 + animations: ^1.0.0+3 + stream_chat: ^0.1.6 + +dev_dependencies: + pedantic: ^1.8.0+1