remove dart client example
@@ -1 +0,0 @@
|
|||||||
# Stream Chat Example App
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
gradle-wrapper.jar
|
|
||||||
/.gradle
|
|
||||||
/captures/
|
|
||||||
/gradlew
|
|
||||||
/gradlew.bat
|
|
||||||
/local.properties
|
|
||||||
GeneratedPluginRegistrant.java
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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.stream_chat"
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.stream_chat">
|
|
||||||
<!-- Flutter needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.stream_chat">
|
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
|
||||||
additional functionality it is fine to subclass or reimplement
|
|
||||||
FlutterApplication and put your custom class here. -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<application
|
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
|
||||||
android:label="stream_chat"
|
|
||||||
android:icon="@mipmap/ic_launcher">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<!-- Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.example.stream_chat
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="@android:color/white" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
||||||
|
Before Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
Flutter draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.stream_chat">
|
|
||||||
<!-- Flutter needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
|
||||||
android.enableR8=true
|
|
||||||
android.useAndroidX=true
|
|
||||||
android.enableJetifier=true
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
*.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
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>App</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>io.flutter.flutter.app</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>App</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>FMWK</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>8.0</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
|
||||||
#include "Generated.xcconfig"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
|
||||||
#include "Generated.xcconfig"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "group:Runner.xcodeproj">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1020"
|
|
||||||
version = "1.3">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
|
||||||
BuildableName = "Runner.app"
|
|
||||||
BlueprintName = "Runner"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
||||||
<Testables>
|
|
||||||
</Testables>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
|
||||||
BuildableName = "Runner.app"
|
|
||||||
BlueprintName = "Runner"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
|
||||||
BuildableName = "Runner.app"
|
|
||||||
BlueprintName = "Runner"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<AdditionalOptions>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Profile"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
|
||||||
BuildableName = "Runner.app"
|
|
||||||
BlueprintName = "Runner"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "group:Runner.xcodeproj">
|
|
||||||
</FileRef>
|
|
||||||
<FileRef
|
|
||||||
location = "group:Pods/Pods.xcodeproj">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "1x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 564 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "[email protected]",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
@@ -1,5 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--View Controller-->
|
|
||||||
<scene sceneID="EHf-IW-A2E">
|
|
||||||
<objects>
|
|
||||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
||||||
<layoutGuides>
|
|
||||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
|
||||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
|
||||||
</layoutGuides>
|
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
|
||||||
</imageView>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="53" y="375"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
<resources>
|
|
||||||
<image name="LaunchImage" width="168" height="185"/>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--Flutter View Controller-->
|
|
||||||
<scene sceneID="tne-QT-ifu">
|
|
||||||
<objects>
|
|
||||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
|
||||||
<layoutGuides>
|
|
||||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
|
||||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
|
||||||
</layoutGuides>
|
|
||||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
</document>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>stream_chat</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#import "GeneratedPluginRegistrant.h"
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
|
||||||
import 'package:timeago/timeago.dart' as timeago;
|
|
||||||
|
|
||||||
import './channel_name_text.dart';
|
|
||||||
import 'channel_image.dart';
|
|
||||||
import 'stream_channel.dart';
|
|
||||||
|
|
||||||
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|
||||||
final bool showBackButton;
|
|
||||||
final VoidCallback onBackPressed;
|
|
||||||
|
|
||||||
ChannelHeader({
|
|
||||||
Key key,
|
|
||||||
this.showBackButton = true,
|
|
||||||
this.onBackPressed,
|
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final streamChat = StreamChannel.of(context);
|
|
||||||
return AppBar(
|
|
||||||
leading: showBackButton ? _buildBackButton(context) : Container(),
|
|
||||||
actions: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 10.0),
|
|
||||||
child: ChannelImage(channel: streamChat.channel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
centerTitle: true,
|
|
||||||
title: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
ChannelNameText(
|
|
||||||
channel: streamChat.channel,
|
|
||||||
),
|
|
||||||
_buildLastActive(context, streamChat.channel),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StatelessWidget _buildLastActive(BuildContext context, Channel channel) {
|
|
||||||
return (channel.lastMessageAt != null)
|
|
||||||
? Text(
|
|
||||||
'Active ${timeago.format(channel.lastMessageAt)}',
|
|
||||||
style: Theme.of(context).textTheme.caption,
|
|
||||||
)
|
|
||||||
: Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
Padding _buildBackButton(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(14.0),
|
|
||||||
child: RawMaterialButton(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
|
||||||
elevation: 0,
|
|
||||||
highlightElevation: 0,
|
|
||||||
focusElevation: 0,
|
|
||||||
disabledElevation: 0,
|
|
||||||
hoverElevation: 0,
|
|
||||||
onPressed: () {
|
|
||||||
if (onBackPressed != null) {
|
|
||||||
onBackPressed();
|
|
||||||
} else {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fillColor: Colors.black.withOpacity(.1),
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
child: Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
size: 15,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
final Size preferredSize;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
|
||||||
|
|
||||||
import './connection_indicator.dart';
|
|
||||||
import './message_page.dart';
|
|
||||||
import './stream_channel.dart';
|
|
||||||
import 'channel_list_view.dart';
|
|
||||||
import 'channel_page_app_bar.dart';
|
|
||||||
import 'stream_chat.dart';
|
|
||||||
|
|
||||||
class ChannelListPage extends StatefulWidget {
|
|
||||||
ChannelListPage({
|
|
||||||
this.filter,
|
|
||||||
this.options,
|
|
||||||
this.sort,
|
|
||||||
this.pagination,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Map<String, dynamic> filter;
|
|
||||||
final Map<String, dynamic> options;
|
|
||||||
final List<SortOption> sort;
|
|
||||||
final PaginationParams pagination;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ChannelListPageState createState() => ChannelListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelListPageState extends State<ChannelListPage> {
|
|
||||||
String _selectedChannelId;
|
|
||||||
bool showSplit;
|
|
||||||
IndicatorController _indicatorController = IndicatorController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
showSplit = MediaQuery.of(context).size.width > 1000;
|
|
||||||
return Flex(
|
|
||||||
direction: Axis.horizontal,
|
|
||||||
children: <Widget>[
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Scaffold(
|
|
||||||
bottomNavigationBar: ConnectionIndicator(
|
|
||||||
indicatorController: _indicatorController,
|
|
||||||
),
|
|
||||||
appBar: ChannelPageAppBar(),
|
|
||||||
body: ChannelListView(
|
|
||||||
channelWidget: MessagePage(),
|
|
||||||
options: widget.options,
|
|
||||||
filter: widget.filter,
|
|
||||||
pagination: widget.pagination,
|
|
||||||
sort: widget.sort,
|
|
||||||
onChannelTap: showSplit
|
|
||||||
? (channelClient, _) {
|
|
||||||
_navigateToChannel(context, channelClient);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () {},
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
child: Icon(
|
|
||||||
Icons.send,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
showSplit ? _buildMessageView(context) : Container(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Flexible _buildMessageView(BuildContext context) {
|
|
||||||
return Flexible(
|
|
||||||
flex: 2,
|
|
||||||
child: _selectedChannelId == null
|
|
||||||
? Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text(
|
|
||||||
'Pick a channel to show the messages 💬',
|
|
||||||
style: Theme.of(context).textTheme.headline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: StreamChannel(
|
|
||||||
channelClient: StreamChat.of(context)
|
|
||||||
.client
|
|
||||||
.state
|
|
||||||
.channels
|
|
||||||
.firstWhere((c) => c.id == _selectedChannelId),
|
|
||||||
child: MessagePage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _navigateToChannel(
|
|
||||||
BuildContext context,
|
|
||||||
Channel channel,
|
|
||||||
) {
|
|
||||||
setState(() {
|
|
||||||
_selectedChannelId = channel.id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final streamChat = StreamChat.of(context);
|
|
||||||
streamChat.client.wsConnectionStatus.addListener(() {
|
|
||||||
if (streamChat.client.wsConnectionStatus.value ==
|
|
||||||
ConnectionStatus.disconnected) {
|
|
||||||
_indicatorController.showIndicator(
|
|
||||||
duration: Duration(minutes: 1),
|
|
||||||
color: Colors.red,
|
|
||||||
text: 'Disconnected',
|
|
||||||
);
|
|
||||||
} else if (streamChat.client.wsConnectionStatus.value ==
|
|
||||||
ConnectionStatus.connecting) {
|
|
||||||
_indicatorController.showIndicator(
|
|
||||||
duration: Duration(minutes: 1),
|
|
||||||
color: Colors.yellow,
|
|
||||||
text: 'Reconnecting',
|
|
||||||
);
|
|
||||||
} else if (streamChat.client.wsConnectionStatus.value ==
|
|
||||||
ConnectionStatus.connected) {
|
|
||||||
_indicatorController.showIndicator(
|
|
||||||
duration: Duration(seconds: 5),
|
|
||||||
color: Colors.green,
|
|
||||||
text: 'Connected',
|
|
||||||
);
|
|
||||||
streamChat.clearChannels();
|
|
||||||
|
|
||||||
streamChat.queryChannels();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
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(Channel, 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<String, dynamic> filter;
|
|
||||||
final Map<String, dynamic> options;
|
|
||||||
final List<SortOption> 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<ChannelListView> {
|
|
||||||
@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<List<Channel>>(
|
|
||||||
stream: streamChat.client.state.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<String> valueKey = key;
|
|
||||||
final index = channelsStates
|
|
||||||
.indexWhere((cs) => 'CHANNEL-${cs.id}' == valueKey.value);
|
|
||||||
return index != -1 ? (index * 2) : null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemBuilder(context, int i, List<Channel> 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.state.channels
|
|
||||||
.firstWhere((c) => c.cid == channelState.cid);
|
|
||||||
|
|
||||||
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<String>('CHANNEL-${channelClient?.id}'),
|
|
||||||
child: child,
|
|
||||||
channelClient: channelClient,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _buildQueryProgressIndicator(context, streamChat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQueryProgressIndicator(context, StreamChat streamChat) {
|
|
||||||
return StreamBuilder<bool>(
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ChannelPageAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|
||||||
ChannelPageAppBar({
|
|
||||||
Key key,
|
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SafeArea(
|
|
||||||
child: Material(
|
|
||||||
elevation: 4,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 30),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withAlpha(5),
|
|
||||||
borderRadius: BorderRadius.circular(32.0),
|
|
||||||
border: Border.all(color: Colors.black.withOpacity(.2))),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: TextField(
|
|
||||||
style: Theme.of(context).textTheme.body1,
|
|
||||||
autofocus: false,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Search',
|
|
||||||
prefixText: ' ',
|
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
final Size preferredSize;
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:jiffy/jiffy.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(Channel) onTap;
|
|
||||||
|
|
||||||
const ChannelPreview({
|
|
||||||
Key key,
|
|
||||||
this.onTap,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final streamChannel = StreamChannel.of(context);
|
|
||||||
return _buildChannelPreview(
|
|
||||||
context,
|
|
||||||
streamChannel,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamChannel _buildChannelPreview(
|
|
||||||
BuildContext context,
|
|
||||||
StreamChannelState streamChannel,
|
|
||||||
) {
|
|
||||||
final channelClient = StreamChat.of(context)
|
|
||||||
.client
|
|
||||||
.state
|
|
||||||
.channels
|
|
||||||
.firstWhere((c) => c.cid == streamChannel.channel.cid);
|
|
||||||
return StreamChannel(
|
|
||||||
channelClient: channelClient,
|
|
||||||
child: ListTile(
|
|
||||||
onTap: () {
|
|
||||||
onTap(channelClient);
|
|
||||||
},
|
|
||||||
leading: ChannelImage(
|
|
||||||
channel: streamChannel.channel,
|
|
||||||
),
|
|
||||||
title: ChannelNameText(
|
|
||||||
channel: streamChannel.channel,
|
|
||||||
),
|
|
||||||
subtitle: _buildSubtitle(
|
|
||||||
streamChannel,
|
|
||||||
),
|
|
||||||
trailing: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
_buildDate(context, streamChannel.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 = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
|
|
||||||
} else {
|
|
||||||
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(
|
|
||||||
stringDate,
|
|
||||||
style: Theme.of(context).textTheme.caption,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSubtitle(
|
|
||||||
StreamChannelState streamChannel,
|
|
||||||
) {
|
|
||||||
return StreamBuilder<List<User>>(
|
|
||||||
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.channel.state.messages.isNotEmpty
|
|
||||||
? streamChannel.channel.state.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<User> 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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ConnectionIndicator extends StatefulWidget {
|
|
||||||
final IndicatorController indicatorController;
|
|
||||||
|
|
||||||
const ConnectionIndicator({
|
|
||||||
Key key,
|
|
||||||
this.indicatorController,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ConnectionIndicatorState createState() => _ConnectionIndicatorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionIndicatorState extends State<ConnectionIndicator> {
|
|
||||||
double _height = 0;
|
|
||||||
String _text;
|
|
||||||
Color _color;
|
|
||||||
VoidCallback _listener;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedContainer(
|
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
height: _height,
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Material(
|
|
||||||
color: _color ?? Theme.of(context).snackBarTheme.backgroundColor,
|
|
||||||
child: Center(
|
|
||||||
child: Text(_text ?? ''),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_listener = () {
|
|
||||||
final values = widget.indicatorController.indicatorValues.value;
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_height = 30;
|
|
||||||
_text = values.text;
|
|
||||||
_color = values.color;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.duration != null) {
|
|
||||||
Future.delayed(values.duration, () {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_height = 0;
|
|
||||||
_text = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
widget.indicatorController.indicatorValues.addListener(_listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.indicatorController.indicatorValues.removeListener(_listener);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IndicatorValues {
|
|
||||||
final String text;
|
|
||||||
final Color color;
|
|
||||||
final Duration duration;
|
|
||||||
|
|
||||||
IndicatorValues({
|
|
||||||
this.duration,
|
|
||||||
this.text,
|
|
||||||
this.color,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class IndicatorController {
|
|
||||||
ValueNotifier<IndicatorValues> indicatorValues = ValueNotifier(null);
|
|
||||||
|
|
||||||
void showIndicator({
|
|
||||||
Duration duration,
|
|
||||||
Color color,
|
|
||||||
String text,
|
|
||||||
}) {
|
|
||||||
indicatorValues.value = IndicatorValues(
|
|
||||||
text: text,
|
|
||||||
color: color,
|
|
||||||
duration: duration,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
|
||||||
|
|
||||||
import 'channel_list_page.dart';
|
|
||||||
import 'stream_chat.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
final client = Client(
|
|
||||||
"qk4nn7rpcn75",
|
|
||||||
logLevel: Level.INFO,
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.setUser(
|
|
||||||
User(id: "wild-breeze-7"),
|
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo',
|
|
||||||
);
|
|
||||||
|
|
||||||
runApp(StreamChat(
|
|
||||||
child: MyApp(),
|
|
||||||
client: client,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_MyAppState createState() => _MyAppState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyAppState extends State<MyApp> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
title: 'Stream Chat Example',
|
|
||||||
home: ChatLoader(),
|
|
||||||
theme: ThemeData(
|
|
||||||
scaffoldBackgroundColor: Color(0xfff1f1f3),
|
|
||||||
primaryColor: Color(0xfff1f1f3),
|
|
||||||
accentColor: Color(0xff006bff),
|
|
||||||
iconTheme: IconThemeData(
|
|
||||||
color: Color(0xff006bff),
|
|
||||||
),
|
|
||||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
|
||||||
foregroundColor: Color(0xff006bff),
|
|
||||||
),
|
|
||||||
backgroundColor: Color(0xfff1f1f3),
|
|
||||||
canvasColor: Color(0xfff1f1f3),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
StreamChat.of(context).dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatLoader extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final streamChat = StreamChat.of(context);
|
|
||||||
return StreamBuilder<User>(
|
|
||||||
stream: streamChat.userStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text('${snapshot.error}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (!snapshot.hasData) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return ChannelListPage(
|
|
||||||
filter: {
|
|
||||||
'members': {
|
|
||||||
'\$in': [StreamChat.of(context).user.id],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sort: [SortOption("last_message_at")],
|
|
||||||
pagination: PaginationParams(
|
|
||||||
limit: 20,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
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<MessageInput> {
|
|
||||||
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: <Widget>[
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
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<MessageListView> {
|
|
||||||
static const _newMessageLoadingOffset = 100;
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
bool _isBottom = true;
|
|
||||||
bool _topWasVisible = false;
|
|
||||||
List<Message> _messages = [];
|
|
||||||
List<Message> _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<ScrollNotification>(
|
|
||||||
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: <Widget>[
|
|
||||||
MessageWidget(
|
|
||||||
key: ValueKey<String>(
|
|
||||||
'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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>('MESSAGE-${message.id}'),
|
|
||||||
previousMessage: previousMessage,
|
|
||||||
message: message,
|
|
||||||
nextMessage: nextMessage,
|
|
||||||
onThreadSelect: widget.onThreadSelect,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: _messages.length + 2,
|
|
||||||
findChildIndexCallback: (key) {
|
|
||||||
final ValueKey<String> 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<bool>(
|
|
||||||
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<String>('TOP-MESSAGE'),
|
|
||||||
child: MessageWidget(
|
|
||||||
key: ValueKey<String>('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<String>('BOTTOM-MESSAGE'),
|
|
||||||
onVisibilityChanged: (visibility) {
|
|
||||||
_isBottom = visibility.visibleBounds != Rect.zero;
|
|
||||||
if (_isBottom) {
|
|
||||||
if (channelBloc.channelClient.state.unreadCount > 0) {
|
|
||||||
channelBloc.channelClient.markRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: MessageWidget(
|
|
||||||
key: ValueKey<String>('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<List<Message>> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
|
||||||
|
|
||||||
import './channel_header.dart';
|
|
||||||
import 'connection_indicator.dart';
|
|
||||||
import 'message_input.dart';
|
|
||||||
import 'message_list_view.dart';
|
|
||||||
import 'stream_channel.dart';
|
|
||||||
import 'stream_chat.dart';
|
|
||||||
|
|
||||||
class MessagePage extends StatefulWidget {
|
|
||||||
final PreferredSizeWidget _channelHeader;
|
|
||||||
|
|
||||||
const MessagePage({
|
|
||||||
Key key,
|
|
||||||
PreferredSizeWidget channelHeader,
|
|
||||||
this.parentMessage,
|
|
||||||
}) : _channelHeader = channelHeader,
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
final Message parentMessage;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MessagePageState createState() => _MessagePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MessagePageState extends State<MessagePage> {
|
|
||||||
IndicatorController _indicatorController = IndicatorController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
resizeToAvoidBottomInset: true,
|
|
||||||
appBar: widget._channelHeader ?? ChannelHeader(),
|
|
||||||
body: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
ConnectionIndicator(
|
|
||||||
indicatorController: _indicatorController,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: MessageListView(
|
|
||||||
parentMessage: widget.parentMessage,
|
|
||||||
onThreadSelect: (message) {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
PageRouteBuilder(
|
|
||||||
pageBuilder: (_, __, ___) => StreamChannel(
|
|
||||||
channelClient: StreamChannel.of(context).channelClient,
|
|
||||||
child: MessagePage(
|
|
||||||
parentMessage: message,
|
|
||||||
channelHeader: widget._channelHeader,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
transitionsBuilder: (
|
|
||||||
_,
|
|
||||||
animation,
|
|
||||||
secondaryAnimation,
|
|
||||||
child,
|
|
||||||
) =>
|
|
||||||
SharedAxisTransition(
|
|
||||||
child: child,
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MessageInput(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final streamChat = StreamChat.of(context);
|
|
||||||
streamChat.client.wsConnectionStatus.addListener(() {
|
|
||||||
if (streamChat.client.wsConnectionStatus.value ==
|
|
||||||
ConnectionStatus.disconnected) {
|
|
||||||
_indicatorController.showIndicator(
|
|
||||||
duration: Duration(minutes: 1),
|
|
||||||
color: Colors.red,
|
|
||||||
text: 'Disconnected',
|
|
||||||
);
|
|
||||||
} else if (streamChat.client.wsConnectionStatus.value ==
|
|
||||||
ConnectionStatus.connecting) {
|
|
||||||
_indicatorController.showIndicator(
|
|
||||||
duration: Duration(minutes: 1),
|
|
||||||
color: Colors.yellow,
|
|
||||||
text: 'Reconnecting',
|
|
||||||
);
|
|
||||||
} else if (streamChat.client.wsConnectionStatus.value ==
|
|
||||||
ConnectionStatus.connected) {
|
|
||||||
_indicatorController.showIndicator(
|
|
||||||
duration: Duration(seconds: 5),
|
|
||||||
color: Colors.green,
|
|
||||||
text: 'Connected',
|
|
||||||
);
|
|
||||||
|
|
||||||
final streamChat = StreamChannel.of(context);
|
|
||||||
streamChat.queryMessages();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
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<MessageWidget>
|
|
||||||
with AutomaticKeepAliveClientMixin {
|
|
||||||
final Map<String, ChangeNotifier> _videoControllers = {};
|
|
||||||
final Map<String, ChangeNotifier> _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 = <Widget>[
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
_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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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<String>(
|
|
||||||
'ATTACHMENT-${attachment.title}-${widget.message.id}'),
|
|
||||||
controller: chewieController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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 Channel channelClient;
|
|
||||||
|
|
||||||
static StreamChannelState of(BuildContext context) {
|
|
||||||
StreamChannelState streamChannelState;
|
|
||||||
|
|
||||||
streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
|
|
||||||
|
|
||||||
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<StreamChannel> {
|
|
||||||
StreamChannelState();
|
|
||||||
|
|
||||||
Channel get channelClient => widget.channelClient;
|
|
||||||
|
|
||||||
Channel get channel => widget.channelClient;
|
|
||||||
|
|
||||||
Stream<ChannelState> get channelStateStream =>
|
|
||||||
widget.channelClient.state.channelStateStream;
|
|
||||||
|
|
||||||
final BehaviorSubject<bool> _queryMessageController = BehaviorSubject();
|
|
||||||
|
|
||||||
Stream<bool> get queryMessage => _queryMessageController.stream;
|
|
||||||
|
|
||||||
void queryMessages() {
|
|
||||||
_queryMessageController.add(true);
|
|
||||||
|
|
||||||
String firstId;
|
|
||||||
if (channel.state.messages.isNotEmpty) {
|
|
||||||
firstId = channel.state.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<void> 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<bool>(
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
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<StreamSubscription> _subscriptions = [];
|
|
||||||
|
|
||||||
StreamChat({
|
|
||||||
Key key,
|
|
||||||
@required this.client,
|
|
||||||
@required Widget child,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
child: child,
|
|
||||||
) {
|
|
||||||
_subscriptions.add(client.on(EventType.messageNew).listen((Event e) {
|
|
||||||
final index = channels.indexWhere((c) => c.cid == e.cid);
|
|
||||||
if (index > 0) {
|
|
||||||
final channel = channels.removeAt(index);
|
|
||||||
channels.insert(0, channel);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
User get user => client.state.user;
|
|
||||||
|
|
||||||
Stream<User> get userStream => client.state.userStream;
|
|
||||||
|
|
||||||
final List<Channel> channels = [];
|
|
||||||
|
|
||||||
final BehaviorSubject<bool> _queryChannelsLoadingController =
|
|
||||||
BehaviorSubject.seeded(false);
|
|
||||||
|
|
||||||
Stream<bool> get queryChannelsLoading =>
|
|
||||||
_queryChannelsLoadingController.stream;
|
|
||||||
|
|
||||||
Future<void> queryChannels({
|
|
||||||
Map<String, dynamic> filter,
|
|
||||||
List<SortOption> sortOptions,
|
|
||||||
PaginationParams paginationParams,
|
|
||||||
Map<String, dynamic> options,
|
|
||||||
}) async {
|
|
||||||
if (_queryChannelsLoadingController.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_queryChannelsLoadingController.sink.add(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
client.queryChannels(
|
|
||||||
filter: filter,
|
|
||||||
sort: sortOptions,
|
|
||||||
options: options,
|
|
||||||
paginationParams: paginationParams,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
_queryChannelsLoadingController.sink.add(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearChannels() {
|
|
||||||
channels.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
client.dispose();
|
|
||||||
_subscriptions.forEach((s) => s.cancel());
|
|
||||||
_queryChannelsLoadingController.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static StreamChat of(BuildContext context, [bool listen = false]) {
|
|
||||||
StreamChat streamChat;
|
|
||||||
|
|
||||||
if (listen) {
|
|
||||||
streamChat = context.dependOnInheritedWidgetOfExactType<StreamChat>();
|
|
||||||
} else {
|
|
||||||
streamChat = context.findAncestorWidgetOfExactType<StreamChat>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamChat == null) {
|
|
||||||
throw Exception(
|
|
||||||
'You must have a StreamChat widget at the top of your widget tree');
|
|
||||||
}
|
|
||||||
|
|
||||||
return streamChat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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: this.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]
|
|
||||||
: ''),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
name: stream_chat_example
|
|
||||||
description: example Flutter project using Stream Chat service
|
|
||||||
|
|
||||||
version: 1.0.0+1
|
|
||||||
|
|
||||||
environment:
|
|
||||||
sdk: ">=2.1.0 <3.0.0"
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_widgets: ^0.1.11
|
|
||||||
timeago: ^2.0.26
|
|
||||||
jiffy: ^3.0.1
|
|
||||||
date_format: ^1.0.8
|
|
||||||
cached_network_image: ^2.0.0
|
|
||||||
flutter_markdown: ^0.5.2
|
|
||||||
url_launcher: ^5.4.2
|
|
||||||
video_player: ^1.0.1
|
|
||||||
chewie: 0.12.1+1
|
|
||||||
animations: ^1.0.0+5
|
|
||||||
|
|
||||||
stream_chat:
|
|
||||||
path: ../
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
build_runner: any
|
|
||||||
build_web_compilers: any
|
|
||||||
|
|
||||||
flutter:
|
|
||||||
uses-material-design: true
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility that Flutter provides. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:stream_chat_example/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(MyApp());
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
||||||
<meta name="description" content="A new Flutter project.">
|
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="apple-mobile-web-status-bar-style" content="black">
|
|
||||||
<meta name="apple-mobile-web-app-title" content="stream_chat">
|
|
||||||
<link rel="apple-touch-icon" href="/icons/Icon-192.png">
|
|
||||||
|
|
||||||
<title>stream_chat</title>
|
|
||||||
<link rel="manifest" href="/manifest.json">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script src="main.dart.js" type="application/javascript"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "stream_chat",
|
|
||||||
"short_name": "stream_chat",
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "minimal-ui",
|
|
||||||
"background_color": "#0175C2",
|
|
||||||
"theme_color": "#0175C2",
|
|
||||||
"description": "A new Flutter project.",
|
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"prefer_related_applications": false,
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/Icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||