Merge pull request #215 from GetStream/feature/lint-cleanup
Lint and pub clean up
@@ -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.3.4
|
||||
url_launcher: ^5.4.2
|
||||
video_player: ^0.10.8+1
|
||||
chewie: ^0.9.10
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,7 +14,11 @@ import 'web_socket_channel_stub.dart'
|
||||
if (dart.library.html) 'web_socket_channel_html.dart'
|
||||
if (dart.library.io) 'web_socket_channel_io.dart';
|
||||
|
||||
/// Typedef which exposes an [Event] as the only parameter.
|
||||
typedef EventHandler = void Function(Event);
|
||||
|
||||
/// Typedef used for connecting to a websocket. Method returns a [WebSocketChannel]
|
||||
/// and accepts a connection [url] and an optional [Iterable] of `protocols`.
|
||||
typedef ConnectWebSocket = WebSocketChannel Function(String url,
|
||||
{Iterable<String> protocols});
|
||||
|
||||
@@ -236,7 +240,9 @@ class WebSocket {
|
||||
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
logger.log(Level.SEVERE, e.toString());
|
||||
}
|
||||
await Future.delayed(
|
||||
Duration(seconds: min(_retryAttempt * 5, 25)),
|
||||
() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/own_user.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:pedantic/pedantic.dart' show unawaited;
|
||||
|
||||
import 'api/channel.dart';
|
||||
import 'api/connection_status.dart';
|
||||
@@ -25,8 +26,15 @@ import 'models/event.dart';
|
||||
import 'models/message.dart';
|
||||
import 'models/user.dart';
|
||||
|
||||
/// Handler function used for logging records. Function requires a single [LogRecord]
|
||||
/// as the only parameter.
|
||||
typedef LogHandlerFunction = void Function(LogRecord record);
|
||||
|
||||
/// Used for decoding [Map] data to a generic type `T`.
|
||||
typedef DecoderFunction<T> = T Function(Map<String, dynamic>);
|
||||
|
||||
/// A function which can be used to request a Stream Chat API token from your
|
||||
/// own backend server. Function requires a single [userId].
|
||||
typedef TokenProvider = Future<String> Function(String userId);
|
||||
|
||||
/// The key used to save the userId to sharedPreferences
|
||||
@@ -462,7 +470,7 @@ class Client {
|
||||
|
||||
if (value == ConnectionStatus.connected &&
|
||||
state.channels?.isNotEmpty == true) {
|
||||
queryChannels(filter: {
|
||||
unawaited(queryChannels(filter: {
|
||||
'cid': {
|
||||
'\$in': state.channels.keys.toList(),
|
||||
},
|
||||
@@ -474,7 +482,7 @@ class Client {
|
||||
online: true,
|
||||
));
|
||||
},
|
||||
);
|
||||
));
|
||||
} else {
|
||||
_synced = false;
|
||||
}
|
||||
@@ -712,7 +720,7 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
_parseError(DioError error) {
|
||||
dynamic _parseError(DioError error) {
|
||||
if (error.type == DioErrorType.RESPONSE) {
|
||||
final apiError =
|
||||
ApiError(error.response?.data, error.response?.statusCode);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//ignore_for_file: public_member_api_docs
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,7 +10,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat/src/db/offline_storage.dart';
|
||||
|
||||
class SharedDB {
|
||||
static constructDatabase(dbName) async {
|
||||
static Future<VmDatabase> constructDatabase(dbName) async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final path = join(dir.path, dbName);
|
||||
final file = File(path);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//ignore_for_file: public_member_api_docs
|
||||
//ignore_for_file: always_declare_return_types
|
||||
class SharedDB {
|
||||
static constructDatabase(dbName) async {
|
||||
print('Unsupported Platform');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//ignore_for_file: public_member_api_docs
|
||||
//ignore_for_file: always_declare_return_types
|
||||
import 'package:moor/moor_web.dart';
|
||||
import 'package:stream_chat/src/db/offline_storage.dart';
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ dependencies:
|
||||
rxdart: ^0.24.1
|
||||
collection: ^1.14.12
|
||||
sqlite3_flutter_libs: ^0.3.0
|
||||
pedantic: ^1.9.2
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^1.10.0
|
||||
@@ -32,5 +33,4 @@ dev_dependencies:
|
||||
moor_generator: ^3.1.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
pedantic: ^1.8.0+1
|
||||
mockito: ^4.1.1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:emojis/emojis.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
@@ -907,8 +907,6 @@ class _GroupInfoScreenState extends State<GroupInfoScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showRemoveUserModal(User user) {}
|
||||
|
||||
Widget _buildConnectedTitleState(User user) {
|
||||
var alternativeWidget;
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class _HomePageState extends State<HomePage> {
|
||||
return <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Stack(
|
||||
overflow: Overflow.visible,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
StreamSvgIcon.message(
|
||||
color: _isSelected(0)
|
||||
@@ -133,7 +133,7 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Stack(
|
||||
overflow: Overflow.visible,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
StreamSvgIcon.mentions(
|
||||
color: _isSelected(1)
|
||||
|
||||
@@ -79,6 +79,9 @@ class AppRoutes {
|
||||
builder: (_) {
|
||||
return GroupInfoScreen();
|
||||
});
|
||||
// Default case, should not reach here.
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
stream_chat_flutter:
|
||||
path: ../
|
||||
stream_chat_flutter: 0.2.13+1
|
||||
flutter_apns: ^1.4.1
|
||||
flutter_local_notifications: ^2.0.2
|
||||
flutter_svg: ^0.19.1
|
||||
|
||||
@@ -160,8 +160,6 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, position) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: Padding(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
@@ -9,8 +7,6 @@ import 'package:dio/dio.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -58,15 +54,16 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
class _ImageFooterState extends State<ImageFooter> {
|
||||
//ignore:unused_field
|
||||
bool _userSearchMode = false;
|
||||
TextEditingController _searchController;
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final FocusNode _messageFocusNode = FocusNode();
|
||||
|
||||
//ignore:unused_field
|
||||
String _channelNameQuery;
|
||||
|
||||
final List<Channel> _selectedChannels = [];
|
||||
bool _loading = false;
|
||||
|
||||
Timer _debounce;
|
||||
|
||||
@@ -311,95 +308,6 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareTextInputSection(modalSetState) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: BottomAppBar(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: _loading
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
focusNode: _messageFocusNode,
|
||||
onChanged: (val) {
|
||||
modalSetState(() {});
|
||||
},
|
||||
onTap: () {
|
||||
modalSetState(() {});
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixText: ' ',
|
||||
hintText: 'Add a comment',
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: BorderSide(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.16),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: BorderSide(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.16),
|
||||
)),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
IconTheme(
|
||||
data: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.messageInputButtonIconTheme,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
modalSetState(() => _loading = true);
|
||||
await sendMessage();
|
||||
modalSetState(() => _loading = false);
|
||||
},
|
||||
splashRadius: 24,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Transform.rotate(
|
||||
angle: -pi / 2,
|
||||
child: StreamSvgIcon.Icon_send_message(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends the current message
|
||||
Future sendMessage() async {
|
||||
var text = _messageController.text.trim();
|
||||
@@ -420,25 +328,6 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
_selectedChannels.clear();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
Future<void> _saveImage(String url) async {
|
||||
var response = await Dio()
|
||||
.get(url, options: Options(responseType: ResponseType.bytes));
|
||||
final result = await ImageGallerySaver.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 60,
|
||||
name: "${DateTime.now().millisecondsSinceEpoch}");
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _saveVideo(String url) async {
|
||||
var appDocDir = await getTemporaryDirectory();
|
||||
var savePath =
|
||||
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
|
||||
await Dio().download(url, savePath);
|
||||
final result = await ImageGallerySaver.saveFile(savePath);
|
||||
print(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for clipping textfield prefix icon
|
||||
|
||||
@@ -798,6 +798,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
? StreamChatTheme.of(context).colorTheme.black.withOpacity(0.2)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5);
|
||||
break;
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,6 +1016,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,6 +1042,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
final mediaInfo = await CompressVideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
||||
// ignore: deprecated_member_use
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -1058,6 +1063,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
// ignore: deprecated_member_use
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
@@ -1110,6 +1116,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
print(e);
|
||||
print(s);
|
||||
// ignore: deprecated_member_use
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error adding the attachment: $e'),
|
||||
@@ -2013,6 +2020,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
attachment.file = file;
|
||||
});
|
||||
} else {
|
||||
// ignore: deprecated_member_use
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
|
||||
@@ -220,7 +220,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
overflow: Overflow.visible,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
UserAvatar(
|
||||
onTap: onUserAvatarTap,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
class OptionListTile extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
@@ -491,7 +491,11 @@ class ColorTheme {
|
||||
stops: [0, 1],
|
||||
),
|
||||
this.borderTop = const Effect(
|
||||
sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0.0),
|
||||
sigmaX: 0,
|
||||
sigmaY: -1,
|
||||
color: Color(0xff000000),
|
||||
blur: 0.0,
|
||||
alpha: 0.08),
|
||||
this.borderBottom = const Effect(
|
||||
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0.0, alpha: 0.08),
|
||||
this.shadowIconButton = const Effect(
|
||||
|
||||
@@ -524,6 +524,7 @@ abstract class ListItem {
|
||||
final user = (this as ListUserItem).user;
|
||||
return 'USER-${user.id}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget when({
|
||||
@@ -536,6 +537,7 @@ abstract class ListItem {
|
||||
if (this is ListUserItem) {
|
||||
return userItem((this as ListUserItem).user);
|
||||
}
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
// ignore: deprecated_member_use
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Cannot launch the url'),
|
||||
@@ -34,6 +35,7 @@ Future<bool> showConfirmationDialog(
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
final effect = StreamChatTheme.of(context).colorTheme.borderTop;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -51,8 +53,8 @@ Future<bool> showConfirmationDialog(
|
||||
),
|
||||
SizedBox(height: 36.0),
|
||||
Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
height: 1.0,
|
||||
color: effect.color.withOpacity(effect.alpha ?? 1),
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
@@ -44,6 +44,9 @@ dependencies:
|
||||
transparent_image: ^1.0.0
|
||||
ezanimation: ^0.4.1
|
||||
synchronized: ^2.2.0+2
|
||||
characters: ^1.0.0
|
||||
dio: ^3.0.10
|
||||
path_provider: ^1.6.27
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
@@ -53,7 +56,8 @@ flutter:
|
||||
- animations/
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.9.2
|
||||
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
mockito: ^4.1.3
|
||||
pedantic: ^1.9.2
|
||||
|
||||