Merge pull request #278 from GetStream/release/1.3.0-beta

1.3.0-beta
This commit is contained in:
Salvatore Giordano
2021-02-26 17:44:05 +01:00
committed by GitHub
58 changed files with 3504 additions and 586 deletions
@@ -13,6 +13,7 @@ on:
jobs: jobs:
analyze: analyze:
if: github.base_ref == 'master'
timeout-minutes: 15 timeout-minutes: 15
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+7
View File
@@ -1,3 +1,10 @@
## 1.3.0-beta
- Save pinned messages in offline storage
- Minor fixes
- `StreamClient.QueryChannels` now returns a Stream and fetches the channels from storage before calling the api
- Added `StreamClient.QueryChannelsOnline` and `StreamClient.QueryChannelsOffline` to fetch channels only from online or offline
## 1.2.0-beta ## 1.2.0-beta
- 🛑 **BREAKING** Changed signature of `StreamClient.search` method - 🛑 **BREAKING** Changed signature of `StreamClient.search` method
@@ -1,2 +1 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
@@ -1,2 +1 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
@@ -0,0 +1,495 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
+4 -1
View File
@@ -4,7 +4,10 @@ import 'package:stream_chat/stream_chat.dart';
Future<void> main() async { Future<void> main() async {
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your /// Create a new instance of [StreamChatClient] passing the apikey obtained from your
/// project dashboard. /// project dashboard.
final client = StreamChatClient('b67pax5b2wdq'); final client = StreamChatClient(
'b67pax5b2wdq',
logLevel: Level.INFO,
);
/// Set the current user. In a production scenario, this should be done using /// Set the current user. In a production scenario, this should be done using
/// a backend to generate a user token using our server SDK. /// a backend to generate a user token using our server SDK.
+75 -24
View File
@@ -40,8 +40,6 @@ class Channel {
state = ChannelClientState(this, channelState); state = ChannelClientState(this, channelState);
_initializedCompleter.complete(true); _initializedCompleter.complete(true);
_startCleaning();
_client.logger.info('New Channel instance initialized created'); _client.logger.info('New Channel instance initialized created');
} }
@@ -844,7 +842,6 @@ class Channel {
if (!_initializedCompleter.isCompleted) { if (!_initializedCompleter.isCompleted) {
_initializedCompleter.complete(true); _initializedCompleter.complete(true);
} }
_startCleaning();
} }
/// Stop watching the channel /// Stop watching the channel
@@ -1184,28 +1181,8 @@ class Channel {
)); ));
} }
Timer _cleaningTimer;
void _startCleaning() {
if (config?.typingEvents == false) {
return;
}
_cleaningTimer = Timer.periodic(Duration(milliseconds: 500), (_) {
final now = DateTime.now();
if (_lastTypingEvent != null &&
now.difference(_lastTypingEvent).inSeconds > 1) {
stopTyping();
}
state._clean();
});
}
/// Call this method to dispose the channel client /// Call this method to dispose the channel client
void dispose() { void dispose() {
_cleaningTimer.cancel();
state.dispose(); state.dispose();
} }
@@ -1256,6 +1233,10 @@ class ChannelClientState {
_computeInitialUnread(); _computeInitialUnread();
_startCleaning();
_startCleaningPinnedMessages();
_channel._client.chatPersistenceClient _channel._client.chatPersistenceClient
?.getChannelThreads(_channel.cid) ?.getChannelThreads(_channel.cid)
?.then((threads) { ?.then((threads) {
@@ -1264,7 +1245,9 @@ class ChannelClientState {
_channel._client.chatPersistenceClient _channel._client.chatPersistenceClient
?.getChannelStateByCid(_channel.cid) ?.getChannelStateByCid(_channel.cid)
?.then((state) { ?.then((state) {
updateChannelState(state); // Replacing the persistence state members with the latest `channelState.members`
// as they may have changes over the time.
updateChannelState(state.copyWith(members: channelState.members));
retryFailedMessages(); retryFailedMessages();
}); });
}); });
@@ -1413,6 +1396,15 @@ class ChannelClientState {
..removeWhere((it) => it.userId != userId), ..removeWhere((it) => it.userId != userId),
); );
addMessage(message); addMessage(message);
if (message.pinned == true) {
_channelState = _channelState.copyWith(
pinnedMessages: [
..._channelState.pinnedMessages ?? [],
message,
],
);
}
})); }));
} }
@@ -1502,6 +1494,13 @@ class ChannelClientState {
Stream<List<Message>> get messagesStream => Stream<List<Message>> get messagesStream =>
channelStateStream.map((cs) => cs.messages); channelStateStream.map((cs) => cs.messages);
/// Channel pinned message list
List<Message> get pinnedMessages => _channelState.pinnedMessages?.toList();
/// Channel pinned message list as a stream
Stream<List<Message>> get pinnedMessagesStream =>
channelStateStream.map((cs) => cs.pinnedMessages?.toList());
/// Get channel last message /// Get channel last message
Message get lastMessage => _channelState.messages?.isNotEmpty == true Message get lastMessage => _channelState.messages?.isNotEmpty == true
? _channelState.messages.last ? _channelState.messages.last
@@ -1658,6 +1657,7 @@ class ChannelClientState {
watcherCount: updatedState.watcherCount, watcherCount: updatedState.watcherCount,
members: newMembers, members: newMembers,
read: newReads, read: newReads,
pinnedMessages: updatedState.pinnedMessages,
); );
} }
@@ -1736,6 +1736,50 @@ class ChannelClientState {
})); }));
} }
Timer _cleaningTimer;
void _startCleaning() {
if (_channel.config?.typingEvents == false) {
return;
}
_cleaningTimer = Timer.periodic(Duration(seconds: 1), (_) {
final now = DateTime.now();
if (_channel._lastTypingEvent != null &&
now.difference(_channel._lastTypingEvent).inSeconds > 1) {
_channel.stopTyping();
}
_clean();
});
}
Timer _pinnedMessagesTimer;
void _startCleaningPinnedMessages() {
_pinnedMessagesTimer = Timer.periodic(Duration(seconds: 30), (_) {
final now = DateTime.now();
var expiredMessages = channelState.pinnedMessages
?.where((m) => m.pinExpires?.isBefore(now) == true)
?.toList() ??
[];
if (expiredMessages.isNotEmpty) {
expiredMessages = expiredMessages
.map((m) => m.copyWith(
pinExpires: null,
pinned: false,
pinnedAt: null,
pinnedBy: null,
))
.toList();
updateChannelState(_channelState.copyWith(
pinnedMessages: pinnedMessages.where(_pinIsValid()).toList(),
messages: expiredMessages,
));
}
});
}
void _clean() { void _clean() {
final now = DateTime.now(); final now = DateTime.now();
_typings.forEach((user, lastTypingEvent) { _typings.forEach((user, lastTypingEvent) {
@@ -1759,6 +1803,13 @@ class ChannelClientState {
_channelStateController.close(); _channelStateController.close();
_isUpToDateController.close(); _isUpToDateController.close();
_threadsController.close(); _threadsController.close();
_cleaningTimer.cancel();
_pinnedMessagesTimer.cancel();
_typingEventsController.close(); _typingEventsController.close();
} }
} }
bool Function(Message) _pinIsValid() {
final now = DateTime.now();
return (Message m) => m.pinExpires.isAfter(now);
}
+11 -3
View File
@@ -4,7 +4,7 @@ part 'requests.g.dart';
/// Sorting options /// Sorting options
@JsonSerializable(createFactory: false) @JsonSerializable(createFactory: false)
class SortOption { class SortOption<T> {
/// Ascending order /// Ascending order
static const ASC = 1; static const ASC = 1;
@@ -17,6 +17,10 @@ class SortOption {
/// A sorting direction /// A sorting direction
final int direction; final int direction;
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
final Comparator<T> comparator;
/// Creates a new SortOption instance /// Creates a new SortOption instance
/// ///
/// For example: /// For example:
@@ -24,7 +28,11 @@ class SortOption {
/// // Sort channels by the last message date: /// // Sort channels by the last message date:
/// final sorting = SortOption("last_message_at") /// final sorting = SortOption("last_message_at")
/// ``` /// ```
const SortOption(this.field, {this.direction = DESC}); const SortOption(
this.field, {
this.direction = DESC,
this.comparator,
});
/// Serialize model to json /// Serialize model to json
Map<String, dynamic> toJson() => _$SortOptionToJson(this); Map<String, dynamic> toJson() => _$SortOptionToJson(this);
@@ -67,7 +75,7 @@ class PaginationParams {
/// ``` /// ```
const PaginationParams({ const PaginationParams({
this.limit = 10, this.limit = 10,
this.offset, this.offset = 0,
this.greaterThan, this.greaterThan,
this.greaterThanOrEqual, this.greaterThanOrEqual,
this.lessThan, this.lessThan,
+128 -157
View File
@@ -9,7 +9,9 @@ import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/retry_policy.dart'; import 'package:stream_chat/src/api/retry_policy.dart';
import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/models/attachment_file.dart'; import 'package:stream_chat/src/models/attachment_file.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/own_user.dart'; import 'package:stream_chat/src/models/own_user.dart';
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
import 'package:stream_chat/version.dart'; import 'package:stream_chat/version.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -21,6 +23,7 @@ import 'api/responses.dart';
import 'api/websocket.dart'; import 'api/websocket.dart';
import 'db/chat_persistence_client.dart'; import 'db/chat_persistence_client.dart';
import 'exceptions.dart'; import 'exceptions.dart';
import 'models/channel_state.dart';
import 'models/event.dart'; import 'models/event.dart';
import 'models/message.dart'; import 'models/message.dart';
import 'models/user.dart'; import 'models/user.dart';
@@ -488,7 +491,7 @@ class StreamChatClient {
if (status == ConnectionStatus.connected && if (status == ConnectionStatus.connected &&
state.channels?.isNotEmpty == true) { state.channels?.isNotEmpty == true) {
unawaited(queryChannels(filter: { unawaited(queryChannelsOnline(filter: {
'cid': { 'cid': {
'\$in': state.channels.keys.toList(), '\$in': state.channels.keys.toList(),
}, },
@@ -577,13 +580,52 @@ class StreamChatClient {
final _queryChannelsStreams = <String, Future<List<Channel>>>{}; final _queryChannelsStreams = <String, Future<List<Channel>>>{};
/// Requests channels with a given query. /// Requests channels with a given query.
Future<List<Channel>> queryChannels({ Stream<List<Channel>> queryChannels({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort, List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options, Map<String, dynamic> options,
PaginationParams paginationParams = const PaginationParams(limit: 10), PaginationParams paginationParams = const PaginationParams(limit: 10),
int messageLimit, int messageLimit,
bool onlyOffline = false, bool preferOffline = false,
bool waitForConnect = true,
}) async* {
final hash = base64.encode(utf8.encode(
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$preferOffline',
));
if (_queryChannelsStreams.containsKey(hash)) {
yield await _queryChannelsStreams[hash];
} else {
if (preferOffline) {
final channels = await queryChannelsOffline(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
if (channels.isNotEmpty) yield channels;
}
final newQueryChannelsFuture = queryChannelsOnline(
filter: filter,
sort: sort,
options: options,
paginationParams: paginationParams,
messageLimit: messageLimit,
);
_queryChannelsStreams[hash] = newQueryChannelsFuture;
yield await newQueryChannelsFuture;
}
}
/// Requests channels with a given query from the API.
Future<List<Channel>> queryChannelsOnline({
@required Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options,
int messageLimit,
PaginationParams paginationParams = const PaginationParams(limit: 10),
bool waitForConnect = true, bool waitForConnect = true,
}) async { }) async {
if (waitForConnect) { if (waitForConnect) {
@@ -592,48 +634,13 @@ class StreamChatClient {
await _connectCompleter.future; await _connectCompleter.future;
} }
if (wsConnectionStatus != ConnectionStatus.connected) { if (wsConnectionStatus != ConnectionStatus.connected) {
final errorMessage = throw Exception(
'You cannot use queryChannels without an active connection. Please call `connectUser` to connect the client.'; 'You cannot use queryChannels without an active connection.'
if (persistenceEnabled) { ' Please call `connectUser` to connect the client.',
logger.warning( );
'$errorMessage\nTrying to retrieve channels from the offline storage.');
onlyOffline = true;
} else {
throw Exception(errorMessage);
}
} }
} }
final hash = base64.encode(utf8.encode(
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$onlyOffline'));
if (_queryChannelsStreams.containsKey(hash)) {
return _queryChannelsStreams[hash];
}
final newQueryChannelsStream = _doQueryChannels(
filter: filter,
sort: sort,
options: options,
paginationParams: paginationParams,
messageLimit: messageLimit,
onlyOffline: onlyOffline,
).whenComplete(() {
_queryChannelsStreams.remove(hash);
});
_queryChannelsStreams[hash] = newQueryChannelsStream;
return newQueryChannelsStream;
}
Future<List<Channel>> _doQueryChannels({
@required Map<String, dynamic> filter,
@required List<SortOption> sort,
@required Map<String, dynamic> options,
@required int messageLimit,
PaginationParams paginationParams = const PaginationParams(limit: 10),
bool onlyOffline = false,
}) async {
logger.info('Query channel start'); logger.info('Query channel start');
final defaultOptions = { final defaultOptions = {
'state': true, 'state': true,
@@ -660,86 +667,86 @@ class StreamChatClient {
payload.addAll(paginationParams.toJson()); payload.addAll(paginationParams.toJson());
} }
if (onlyOffline) { final response = await get(
return _queryChannelsOffline( '/channels',
filter: filter, queryParameters: {
sort: sort, 'payload': jsonEncode(payload),
paginationParams: paginationParams, },
); );
}
try { final res = decode<QueryChannelsResponse>(
final response = await get( response.data,
'/channels', QueryChannelsResponse.fromJson,
queryParameters: { );
'payload': jsonEncode(payload),
},
);
final res = decode<QueryChannelsResponse>( if ((res.channels ?? []).isEmpty && (paginationParams?.offset ?? 0) == 0) {
response.data, logger.warning('''We could not find any channel for this query.
QueryChannelsResponse.fromJson,
);
final users = res.channels
?.expand((channel) => channel.members.map((member) => member.user))
?.toList();
if (users != null) {
state._updateUsers(users);
}
logger.info('Got ${res.channels?.length} channels from api');
if (res.channels?.isEmpty != false &&
(paginationParams?.offset ?? 0) == 0) {
logger.warning('''We could not find any channel for this query.
Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial
If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart'''); If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''');
} return <Channel>[];
final newChannels = Map<String, Channel>.from(state.channels ?? {});
final channels = <Channel>[];
if (res.channels != null) {
for (final channelState in res.channels) {
final channel = newChannels[channelState.channel.cid];
if (channel != null) {
channel.state?.updateChannelState(channelState);
channels.add(channel);
} else {
final newChannel = Channel.fromState(this, channelState);
await chatPersistenceClient
?.updateChannelState(newChannel.state.channelState);
newChannel.state?.updateChannelState(channelState);
newChannels[newChannel.cid] = newChannel;
channels.add(newChannel);
}
}
}
state.channels = newChannels;
await chatPersistenceClient?.updateChannelQueries(
filter,
res.channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0,
);
return channels;
} catch (e) {
if (!persistenceEnabled) {
rethrow;
}
return _queryChannelsOffline(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
} }
final channels = res.channels;
final users = channels
.expand((it) => it.members)
.map((it) => it.user)
.toList(growable: false);
state._updateUsers(users);
logger.info('Got ${res.channels?.length} channels from api');
final updateData = _mapChannelStateToChannel(channels);
await chatPersistenceClient?.updateChannelQueries(
filter,
channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0,
);
state.channels = updateData.key;
return updateData.value;
} }
dynamic _parseError(DioError error) { /// Requests channels with a given query from the Persistence client.
Future<List<Channel>> queryChannelsOffline({
@required Map<String, dynamic> filter,
@required List<SortOption<ChannelModel>> sort,
PaginationParams paginationParams = const PaginationParams(limit: 10),
}) async {
final offlineChannels = await chatPersistenceClient?.getChannelStates(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
final updatedData = _mapChannelStateToChannel(offlineChannels);
state.channels = updatedData.key;
return updatedData.value;
}
MapEntry<Map<String, Channel>, List<Channel>> _mapChannelStateToChannel(
List<ChannelState> channelStates,
) {
final channels = {...state.channels ?? {}};
final newChannels = <Channel>[];
if (channelStates != null) {
for (final channelState in channelStates) {
final channel = channels[channelState.channel.cid];
if (channel != null) {
channel.state?.updateChannelState(channelState);
newChannels.add(channel);
} else {
final newChannel = Channel.fromState(this, channelState);
channels[newChannel.cid] = newChannel;
newChannels.add(newChannel);
}
}
}
return MapEntry(channels, newChannels);
}
Object _parseError(DioError error) {
if (error.type == DioErrorType.RESPONSE) { if (error.type == DioErrorType.RESPONSE) {
final apiError = final apiError =
ApiError(error.response?.data, error.response?.statusCode); ApiError(error.response?.data, error.response?.statusCode);
@@ -750,39 +757,6 @@ class StreamChatClient {
return error; return error;
} }
Future<List<Channel>> _queryChannelsOffline({
@required Map<String, dynamic> filter,
@required List<SortOption> sort,
PaginationParams paginationParams = const PaginationParams(limit: 10),
}) async {
final offlineChannels = await chatPersistenceClient?.getChannelStates(
filter: filter,
sort: sort,
paginationParams: paginationParams,
) ??
[];
final newChannels = Map<String, Channel>.from(state.channels ?? {});
logger.info('Got ${offlineChannels.length} channels from storage');
final channels = offlineChannels.map((channelState) {
final channel = newChannels[channelState.channel.cid];
if (channel != null) {
channel.state?.updateChannelState(channelState);
return channel;
} else {
final newChannel = Channel.fromState(this, channelState);
chatPersistenceClient
?.updateChannelState(newChannel.state.channelState);
newChannels[newChannel.cid] = newChannel;
return newChannel;
}
}).toList();
if (channels.isNotEmpty) {
state.channels = newChannels;
}
return channels;
}
/// Handy method to make http GET request with error parsing. /// Handy method to make http GET request with error parsing.
Future<Response<String>> get( Future<Response<String>> get(
String path, { String path, {
@@ -888,9 +862,8 @@ class StreamChatClient {
String get _authType => _anonymous ? 'anonymous' : 'jwt'; String get _authType => _anonymous ? 'anonymous' : 'jwt';
// TODO: get the right version of the lib from the build toolchain
String get _userAgent => String get _userAgent =>
'stream-chat-dart-client-${PACKAGE_VERSION.split('+')[0]}'; 'stream-chat-dart-client-${CurrentPlatform.name}-${PACKAGE_VERSION.split('+')[0]}';
Map<String, String> get _commonQueryParams => { Map<String, String> get _commonQueryParams => {
'user_id': state.user?.id, 'user_id': state.user?.id,
@@ -1448,18 +1421,16 @@ class ClientState {
_userController.add(user); _userController.add(user);
} }
void _updateUsers(List<User> users) { void _updateUsers(List<User> userList) {
users?.forEach(_updateUser);
}
void _updateUser(User user) {
final newUsers = { final newUsers = {
...users ?? {}, ...users ?? {},
user.id: user, for (var user in userList) user.id: user,
}; };
_usersController.add(newUsers); _usersController.add(newUsers);
} }
void _updateUser(User user) => _updateUsers([user]);
/// The current user /// The current user
OwnUser get user => _userController.value; OwnUser get user => _userController.value;
@@ -56,23 +56,31 @@ abstract class ChatPersistenceClient {
PaginationParams messagePagination, PaginationParams messagePagination,
}); });
/// Get stored pinned [Message]s by providing channel [cid]
Future<List<Message>> getPinnedMessagesByCid(
String cid, {
PaginationParams messagePagination,
});
/// Get [ChannelState] data by providing channel [cid] /// Get [ChannelState] data by providing channel [cid]
Future<ChannelState> getChannelStateByCid( Future<ChannelState> getChannelStateByCid(
String cid, { String cid, {
PaginationParams messagePagination, PaginationParams messagePagination,
PaginationParams pinnedMessagePagination,
}) async { }) async {
final members = await getMembersByCid(cid); final data = await Future.wait([
final reads = await getReadsByCid(cid); getMembersByCid(cid),
final channel = await getChannelByCid(cid); getReadsByCid(cid),
final messages = await getMessagesByCid( getChannelByCid(cid),
cid, getMessagesByCid(cid, messagePagination: messagePagination),
messagePagination: messagePagination, getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
); ]);
return ChannelState( return ChannelState(
members: members, members: data[0],
read: reads, read: data[1],
messages: messages, channel: data[2],
channel: channel, messages: data[3],
pinnedMessages: data[4],
); );
} }
@@ -82,7 +90,7 @@ abstract class ChatPersistenceClient {
/// for filtering out states. /// for filtering out states.
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}); });
@@ -101,17 +109,33 @@ abstract class ChatPersistenceClient {
return deleteMessageByIds([messageId]); return deleteMessageByIds([messageId]);
} }
/// Remove a pinned message by [messageId]
Future<void> deletePinnedMessageById(String messageId) {
return deletePinnedMessageByIds([messageId]);
}
/// Remove a message by [messageIds] /// Remove a message by [messageIds]
Future<void> deleteMessageByIds(List<String> messageIds); Future<void> deleteMessageByIds(List<String> messageIds);
/// Remove a pinned message by [messageIds]
Future<void> deletePinnedMessageByIds(List<String> messageIds);
/// Remove a message by channel [cid] /// Remove a message by channel [cid]
Future<void> deleteMessageByCid(String cid) { Future<void> deleteMessageByCid(String cid) {
return deleteMessageByCids([cid]); return deleteMessageByCids([cid]);
} }
/// Remove a pinned message by channel [cid]
Future<void> deletePinnedMessageByCid(String cid) {
return deletePinnedMessageByCids([cid]);
}
/// Remove a message by message [cids] /// Remove a message by message [cids]
Future<void> deleteMessageByCids(List<String> cids); Future<void> deleteMessageByCids(List<String> cids);
/// Remove a pinned message by message [cids]
Future<void> deletePinnedMessageByCids(List<String> cids);
/// Remove a channel by [cid] /// Remove a channel by [cid]
Future<void> deleteChannels(List<String> cids); Future<void> deleteChannels(List<String> cids);
@@ -119,6 +143,10 @@ abstract class ChatPersistenceClient {
/// the new [messages] data /// the new [messages] data
Future<void> updateMessages(String cid, List<Message> messages); Future<void> updateMessages(String cid, List<Message> messages);
/// Updates the pinned message data of a particular channel [cid] with
/// the new [messages] data
Future<void> updatePinnedMessages(String cid, List<Message> messages);
/// Returns all the threads by parent message of a particular channel by /// Returns all the threads by parent message of a particular channel by
/// providing channel [cid] /// providing channel [cid]
Future<Map<String, List<Message>>> getChannelThreads(String cid); Future<Map<String, List<Message>>> getChannelThreads(String cid);
@@ -204,6 +232,12 @@ abstract class ChatPersistenceClient {
return updateMessages(cid, messages.toList(growable: false)); return updateMessages(cid, messages.toList(growable: false));
}).toList(growable: false); }).toList(growable: false);
final updatePinnedMessagesFuture = channelStates.map((it) {
final cid = it.channel.cid;
final messages = it.pinnedMessages.where((it) => it != null);
return updatePinnedMessages(cid, messages.toList(growable: false));
}).toList(growable: false);
final updateReadsFuture = channelStates.map((it) { final updateReadsFuture = channelStates.map((it) {
final cid = it.channel.cid; final cid = it.channel.cid;
final reads = it.read?.where((it) => it != null) ?? []; final reads = it.read?.where((it) => it != null) ?? [];
@@ -218,6 +252,7 @@ abstract class ChatPersistenceClient {
await Future.wait([ await Future.wait([
...updateMessagesFuture, ...updateMessagesFuture,
...updatePinnedMessagesFuture,
...updateReadsFuture, ...updateReadsFuture,
...updateMembersFuture, ...updateMembersFuture,
updateUsers(users.toList(growable: false)), updateUsers(users.toList(growable: false)),
@@ -58,7 +58,7 @@ class Attachment {
final AttachmentFile file; final AttachmentFile file;
/// The current upload state of the attachment /// The current upload state of the attachment
final UploadState uploadState; UploadState uploadState;
/// Map of custom channel extraData /// Map of custom channel extraData
@JsonKey(includeIfNull: false) @JsonKey(includeIfNull: false)
@@ -123,10 +123,15 @@ class Attachment {
this.actions, this.actions,
this.extraData, this.extraData,
this.file, this.file,
this.uploadState, UploadState uploadState,
}) : id = id ?? Uuid().v4(), }) : id = id ?? Uuid().v4(),
title = title ?? file?.name, title = title ?? file?.name,
localUri = file?.path != null ? Uri.parse(file.path) : null; localUri = file?.path != null ? Uri.parse(file.path) : null {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null)
? UploadState.success()
: UploadState.preparing());
}
/// Create a new instance from a json /// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) { factory Attachment.fromJson(Map<String, dynamic> json) {
@@ -8,7 +8,7 @@ import 'message.dart';
part 'channel_state.g.dart'; part 'channel_state.g.dart';
/// The class that contains the information about a command /// The class that contains the information about a channel
@JsonSerializable() @JsonSerializable()
class ChannelState { class ChannelState {
/// The channel to which this state belongs /// The channel to which this state belongs
@@ -0,0 +1,78 @@
import 'platform_detector_stub.dart'
if (dart.library.html) 'platform_detector_web.dart'
if (dart.library.io) 'platform_detector_io.dart';
/// Possible platforms
enum PlatformType {
///
Android,
///
Ios,
///
Web,
///
MacOS,
///
Windows,
///
Linux,
///
Fuchsia,
}
/// Utility class that provides information on the current platform
class CurrentPlatform {
CurrentPlatform._();
/// True if the app is running on android
static bool get isAndroid => type == PlatformType.Android;
/// True if the app is running on ios
static bool get isIos => type == PlatformType.Ios;
/// True if the app is running on web
static bool get isWeb => type == PlatformType.Web;
/// True if the app is running on macos
static bool get isMacOS => type == PlatformType.MacOS;
/// True if the app is running on windows
static bool get isWindows => type == PlatformType.Windows;
/// True if the app is running on linux
static bool get isLinux => type == PlatformType.Linux;
/// True if the app is running on fuchsia
static bool get isFuchsia => type == PlatformType.Fuchsia;
/// Returns a string version of the platform
static String get name {
switch (type) {
case PlatformType.Android:
return 'android';
case PlatformType.Ios:
return 'ios';
case PlatformType.Web:
return 'web';
case PlatformType.MacOS:
return 'macos';
case PlatformType.Windows:
return 'windows';
case PlatformType.Linux:
return 'linux';
case PlatformType.Fuchsia:
return 'fuchsia';
default:
return '';
}
}
/// Get current platform type
static PlatformType get type => currentPlatform;
}
@@ -0,0 +1,12 @@
import 'dart:io';
import 'platform_detector.dart';
/// Version running on native systems
PlatformType get currentPlatform {
if (Platform.isWindows) return PlatformType.Windows;
if (Platform.isFuchsia) return PlatformType.Fuchsia;
if (Platform.isMacOS) return PlatformType.MacOS;
if (Platform.isLinux) return PlatformType.Linux;
if (Platform.isIOS) return PlatformType.Ios;
return PlatformType.Android;
}
@@ -0,0 +1,6 @@
import 'platform_detector.dart';
/// Stub implementation
PlatformType get currentPlatform {
throw UnimplementedError();
}
@@ -0,0 +1,4 @@
import 'platform_detector.dart';
/// Version running on web
PlatformType get currentPlatform => PlatformType.Web;
+1 -1
View File
@@ -2,4 +2,4 @@ import 'package:stream_chat/src/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
const PACKAGE_VERSION = '1.2.0-beta'; const PACKAGE_VERSION = '1.3.0-beta';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. description: The official Dart client for Stream Chat, a service for building chat applications.
version: 1.2.0-beta version: 1.3.0-beta
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -12,7 +12,7 @@ void main() {
test('PaginationParams', () { test('PaginationParams', () {
final option = PaginationParams(); final option = PaginationParams();
final j = option.toJson(); final j = option.toJson();
expect(j, {'limit': 10}); expect(j, {'limit': 10, 'offset': 0});
}); });
}); });
} }
+11 -7
View File
@@ -10,6 +10,7 @@ import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/exceptions.dart'; import 'package:stream_chat/src/exceptions.dart';
import 'package:stream_chat/src/models/message.dart'; import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/user.dart'; import 'package:stream_chat/src/models/user.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class MockDio extends Mock implements DioForNative {} class MockDio extends Mock implements DioForNative {}
@@ -86,7 +87,7 @@ void main() {
}); });
}); });
group('queryChannels', () { group('queryChannelsOnline', () {
test('should pass right default parameters', () async { test('should pass right default parameters', () async {
final mockDio = MockDio(); final mockDio = MockDio();
@@ -106,13 +107,14 @@ void main() {
"watch": true, "watch": true,
"presence": false, "presence": false,
"limit": 10, "limit": 10,
"offset": 0,
}), }),
}; };
when(mockDio.get<String>('/channels', queryParameters: queryParams)) when(mockDio.get<String>('/channels', queryParameters: queryParams))
.thenAnswer((_) async => Response(data: '{}', statusCode: 200)); .thenAnswer((_) async => Response(data: '{}', statusCode: 200));
await client.queryChannels(waitForConnect: false); await client.queryChannelsOnline(filter: null, waitForConnect: false);
verify(mockDio.get<String>('/channels', queryParameters: queryParams)) verify(mockDio.get<String>('/channels', queryParameters: queryParams))
.called(1); .called(1);
@@ -134,7 +136,7 @@ void main() {
"\$in": ["test"], "\$in": ["test"],
}, },
}; };
final sortOptions = <SortOption>[]; final sortOptions = <SortOption<ChannelModel>>[];
final options = {"state": false, "watch": false, "presence": true}; final options = {"state": false, "watch": false, "presence": true};
final paginationParams = PaginationParams( final paginationParams = PaginationParams(
limit: 10, limit: 10,
@@ -152,10 +154,10 @@ void main() {
when(mockDio.get<String>('/channels', queryParameters: queryParams)) when(mockDio.get<String>('/channels', queryParameters: queryParams))
.thenAnswer((_) async { .thenAnswer((_) async {
return Response(data: '{}', statusCode: 200); return Response(data: '{"channels":[]}', statusCode: 200);
}); });
await client.queryChannels( await client.queryChannelsOnline(
filter: queryFilter, filter: queryFilter,
sort: sortOptions, sort: sortOptions,
options: options, options: options,
@@ -229,6 +231,7 @@ void main() {
'query': query, 'query': query,
'sort': sortOptions, 'sort': sortOptions,
'limit': 10, 'limit': 10,
'offset': 0,
}), }),
}; };
@@ -346,7 +349,8 @@ void main() {
}; };
when(mockDio.get<String>('/users', queryParameters: queryParams)) when(mockDio.get<String>('/users', queryParameters: queryParams))
.thenAnswer((_) async => Response(data: '{}', statusCode: 200)); .thenAnswer(
(_) async => Response(data: '{"users":[]}', statusCode: 200));
await client.queryUsers(); await client.queryUsers();
@@ -382,7 +386,7 @@ void main() {
when(mockDio.get<String>('/users', queryParameters: queryParams)) when(mockDio.get<String>('/users', queryParameters: queryParams))
.thenAnswer((_) async { .thenAnswer((_) async {
return Response(data: '{}', statusCode: 200); return Response(data: '{"users":[]}', statusCode: 200);
}); });
await client.queryUsers( await client.queryUsers(
@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:rxdart/rxdart.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:stream_chat/version.dart'; import 'package:stream_chat/version.dart';
+10 -1
View File
@@ -1,7 +1,16 @@
## 1.3.0-beta
- Added `MessageInputTheme`
- Fixed overflow in `MessageInput` animation
- Delete only image on imagegallery
- Close keyboard after sending a command
- Exposed `customAttachmentBuilders` through `MessageListView`
- Update `stream_chat_core` dependency
## 1.2.0-beta ## 1.2.0-beta
- Minor fixes - Minor fixes
- Update stream_chat_core dependency - Update `stream_chat_core` dependency
## 1.1.1-beta ## 1.1.1-beta
@@ -25,8 +25,7 @@ dependencies:
sdk: flutter sdk: flutter
stream_chat_flutter: stream_chat_flutter:
path: ../ path: ../
stream_chat_persistence: stream_chat_persistence: ^1.3.0-beta
path: ../../stream_chat_persistence
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
@@ -27,7 +27,9 @@ class AttachmentUploadStateBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (attachment.uploadState == null) return Offstage(); if (message.status == null || message.status == MessageSendingStatus.sent) {
return Offstage();
}
final messageId = message.id; final messageId = message.id;
final attachmentId = attachment.id; final attachmentId = attachment.id;
@@ -227,6 +227,20 @@ class FileAttachment extends AttachmentWidget {
}, },
); );
if (message != null &&
(message.status == null ||
message.status == MessageSendingStatus.sent)) {
trailingWidget = IconButton(
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
padding: const EdgeInsets.all(8),
visualDensity: VisualDensity.compact,
splashRadius: 16,
onPressed: () {
launchURL(context, attachment.assetUrl);
},
);
}
return Material( return Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: trailingWidget, child: trailingWidget,
@@ -241,6 +255,9 @@ class FileAttachment extends AttachmentWidget {
); );
return attachment.uploadState?.when( return attachment.uploadState?.when(
preparing: () { preparing: () {
if (message == null) {
return Text('${fileSize(size, 2)}', style: textStyle);
}
return UploadProgressIndicator( return UploadProgressIndicator(
uploaded: 0, uploaded: 0,
total: double.maxFinite.toInt(), total: double.maxFinite.toInt(),
@@ -49,8 +49,11 @@ class ChannelInfo extends StatelessWidget {
var alternativeWidget; var alternativeWidget;
if (channel.memberCount != null && channel.memberCount > 2) { if (channel.memberCount != null && channel.memberCount > 2) {
var text = '${channel.memberCount} Members';
final watcherCount = channel.state.watcherCount ?? 0;
if (watcherCount > 0) text += ' $watcherCount Online';
alternativeWidget = Text( alternativeWidget = Text(
'${channel.memberCount} Members, ${channel.state.watcherCount} Online', text,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.channelTheme .channelTheme
.channelHeaderTheme .channelHeaderTheme
@@ -1,6 +1,3 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
@@ -99,7 +96,7 @@ class ChannelListView extends StatefulWidget {
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption> sort; final List<SortOption<ChannelModel>> sort;
/// Pagination parameters /// Pagination parameters
/// limit: the number of channels to return (max is 30) /// limit: the number of channels to return (max is 30)
@@ -159,54 +156,40 @@ class ChannelListView extends StatefulWidget {
_ChannelListViewState createState() => _ChannelListViewState(); _ChannelListViewState createState() => _ChannelListViewState();
} }
class _ChannelListViewState extends State<ChannelListView> class _ChannelListViewState extends State<ChannelListView> {
with WidgetsBindingObserver { final _slideController = SlidableController();
final ScrollController _scrollController = ScrollController();
final SlidableController _slideController = SlidableController(); final _channelListController = ChannelListController();
final ChannelListController _channelListController = ChannelListController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var child = ChannelListCore( Widget child = ChannelListCore(
channelListController: _channelListController,
listBuilder: widget.listBuilder ??
(context, list) {
return _buildListView(list);
},
emptyBuilder: widget.emptyBuilder ??
(BuildContext context) {
return _buildEmptyWidget();
},
errorBuilder: widget.errorBuilder ??
(BuildContext context, dynamic error) {
return _buildErrorWidget(context);
},
loadingBuilder: widget.loadingBuilder ??
(BuildContext context) {
return _buildLoadingWidget();
},
pagination: widget.pagination, pagination: widget.pagination,
options: widget.options, options: widget.options,
sort: widget.sort, sort: widget.sort,
filter: widget.filter, filter: widget.filter,
channelListController: _channelListController,
listBuilder: widget.listBuilder ?? _buildListView,
emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget,
errorBuilder: widget.errorBuilder ?? _buildErrorWidget,
loadingBuilder: widget.loadingBuilder ?? _buildLoadingWidget,
); );
if (!widget.pullToRefresh) { if (widget.pullToRefresh) {
return child; child = RefreshIndicator(
} else { onRefresh: () => _channelListController.loadData(),
return RefreshIndicator(
onRefresh: () async {
_channelListController.loadData();
},
child: child, child: child,
); );
} }
return LazyLoadScrollView(
onEndOfPage: () => _channelListController.paginateData(),
child: child,
);
} }
Widget _buildListView( Widget _buildListView(BuildContext context, List<Channel> channels) {
List<Channel> channels, Widget child;
) {
var child;
if (channels.isNotEmpty) { if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) { if (widget.crossAxisCount > 1) {
@@ -216,7 +199,6 @@ class _ChannelListViewState extends State<ChannelListView>
crossAxisCount: widget.crossAxisCount), crossAxisCount: widget.crossAxisCount),
itemCount: channels.length, itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels); return _gridItemBuilder(context, index, channels);
}, },
@@ -236,18 +218,17 @@ class _ChannelListViewState extends State<ChannelListView>
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _listItemBuilder(context, index, channels); return _listItemBuilder(context, index, channels);
}, },
controller: _scrollController,
); );
} }
} }
return AnimatedSwitcher( return AnimatedSwitcher(
child: child, child: child,
duration: Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
} }
Widget _buildEmptyWidget() { Widget _buildEmptyWidget(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, viewportConstraints) { builder: (context, viewportConstraints) {
return SingleChildScrollView( return SingleChildScrollView(
@@ -326,7 +307,7 @@ class _ChannelListViewState extends State<ChannelListView>
); );
} }
Widget _buildLoadingWidget() { Widget _buildLoadingWidget(BuildContext context) {
return ListView( return ListView(
padding: widget.padding, padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
@@ -341,13 +322,13 @@ class _ChannelListViewState extends State<ChannelListView>
return _separatorBuilder(context, i); return _separatorBuilder(context, i);
} }
} }
return _buildLoadingItem(); return _buildLoadingItem(context);
}, },
), ),
); );
} }
Shimmer _buildLoadingItem() { Shimmer _buildLoadingItem(BuildContext context) {
if (widget.crossAxisCount > 1) { if (widget.crossAxisCount > 1) {
return Shimmer.fromColors( return Shimmer.fromColors(
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
@@ -443,9 +424,7 @@ class _ChannelListViewState extends State<ChannelListView>
} }
} }
Widget _buildErrorWidget( Widget _buildErrorWidget(BuildContext context, Object error) {
BuildContext context,
) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -467,9 +446,7 @@ class _ChannelListViewState extends State<ChannelListView>
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.headline6,
), ),
FlatButton( FlatButton(
onPressed: () { onPressed: () => _channelListController.loadData(),
_channelListController.loadData();
},
child: Text('Retry'), child: Text('Retry'),
), ),
], ],
@@ -572,23 +549,13 @@ class _ChannelListViewState extends State<ChannelListView>
], ],
child: Container( child: Container(
color: StreamChatTheme.of(context).colorTheme.whiteSnow, color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: widget.channelPreviewBuilder != null child: widget.channelPreviewBuilder?.call(context, channel) ??
? widget.channelPreviewBuilder( ChannelPreview(
context, onLongPress: widget.onChannelLongPress,
channel, channel: channel,
) onImageTap: widget.onImageTap?.call(channel),
: ChannelPreview( onTap: (channel) => onTap(channel, widget.channelWidget),
onLongPress: widget.onChannelLongPress, ),
channel: channel,
onImageTap: widget.onImageTap != null
? () {
widget.onImageTap(channel);
}
: null,
onTap: (channel) {
onTap(channel, widget.channelWidget);
},
),
), ),
); );
}, },
@@ -618,9 +585,7 @@ class _ChannelListViewState extends State<ChannelListView>
width: 64, width: 64,
height: 64, height: 64,
), ),
onTap: () { onTap: () => widget.onChannelTap(channel, null),
widget.onChannelTap(channel, null);
},
), ),
SizedBox(height: 7), SizedBox(height: 7),
Padding( Padding(
@@ -680,70 +645,4 @@ class _ChannelListViewState extends State<ChannelListView>
color: effect.color.withOpacity(effect.alpha ?? 1.0), color: effect.color.withOpacity(effect.alpha ?? 1.0),
); );
} }
void _listenChannelPagination(ChannelsBlocState channelsProvider) {
if (_scrollController.position.maxScrollExtent ==
_scrollController.offset &&
_scrollController.offset != 0) {
_channelListController.paginateData();
}
}
StreamSubscription _subscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
_scrollController.addListener(() {
channelsBloc.queryChannelsLoading.first.then((loading) {
if (!loading) {
_listenChannelPagination(channelsBloc);
}
});
});
final client = StreamChat.of(context).client;
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
EventType.notificationMessageNew,
EventType.channelVisible,
)
.listen((event) {
_channelListController.loadData();
});
}
@override
void didUpdateWidget(ChannelListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
_channelListController.loadData();
}
}
@override
void dispose() {
_subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
} }
@@ -103,11 +103,22 @@ class ImageActionsModal extends StatelessWidget {
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
() { () {
Navigator.pop(context); final channel = StreamChannel.of(context).channel;
Navigator.pop(context); if (message.attachments.length > 1 ||
StreamChannel.of(context) message.text.isNotEmpty) {
.channel final remainingAttachments = [...message.attachments]
.deleteMessage(message); ..removeAt(currentIndex);
channel.updateMessage(message.copyWith(
attachments: remainingAttachments,
));
Navigator.pop(context);
Navigator.pop(context);
} else {
channel.deleteMessage(message).then((value) {
Navigator.pop(context);
Navigator.pop(context);
});
}
}, },
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
@@ -596,7 +596,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
elevation: 2, elevation: 2,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: StreamChatTheme.of(context).channelTheme.inputBackground, backgroundColor:
StreamChatTheme.of(context).messageInputTheme.inputBackground,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
@@ -235,7 +235,7 @@ class MessageInputState extends State<MessageInput> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = Container( Widget child = Container(
color: StreamChatTheme.of(context).channelTheme.inputBackground, color: StreamChatTheme.of(context).messageInputTheme.inputBackground,
child: SafeArea( child: SafeArea(
child: GestureDetector( child: GestureDetector(
onPanUpdate: (details) { onPanUpdate: (details) {
@@ -392,7 +392,8 @@ class MessageInputState extends State<MessageInput> {
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: _buildSendButton(context), firstChild: _buildSendButton(context),
secondChild: _buildIdleSendButton(context), secondChild: _buildIdleSendButton(context),
duration: Duration(milliseconds: 300), duration:
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
alignment: Alignment.center, alignment: Alignment.center,
), ),
); );
@@ -417,19 +418,21 @@ class MessageInputState extends State<MessageInput> {
), ),
splashRadius: 24, splashRadius: 24,
), ),
secondChild: Row( secondChild: FittedBox(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
if (!widget.disableAttachments) _buildAttachmentButton(), children: [
if (widget.editMessage == null && if (!widget.disableAttachments) _buildAttachmentButton(),
StreamChannel.of(context) if (widget.editMessage == null &&
.channel StreamChannel.of(context)
?.config .channel
?.commands ?.config
?.isNotEmpty == ?.commands
true) ?.isNotEmpty ==
_buildCommandButton(), true)
].insertBetween(const SizedBox(width: 8)), _buildCommandButton(),
].insertBetween(const SizedBox(width: 8)),
),
), ),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
alignment: Alignment.center, alignment: Alignment.center,
@@ -1683,8 +1686,10 @@ class MessageInputState extends State<MessageInput> {
return IconButton( return IconButton(
icon: StreamSvgIcon.lightning( icon: StreamSvgIcon.lightning(
color: _commandsOverlay != null color: _commandsOverlay != null
? StreamChatTheme.of(context).channelTheme.actionButtonColor ? StreamChatTheme.of(context).messageInputTheme.actionButtonColor
: StreamChatTheme.of(context).channelTheme.actionButtonIdleColor, : StreamChatTheme.of(context)
.messageInputTheme
.actionButtonIdleColor,
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -1721,8 +1726,10 @@ class MessageInputState extends State<MessageInput> {
return IconButton( return IconButton(
icon: StreamSvgIcon.attach( icon: StreamSvgIcon.attach(
color: _openFilePickerSection color: _openFilePickerSection
? StreamChatTheme.of(context).channelTheme.actionButtonColor ? StreamChatTheme.of(context).messageInputTheme.actionButtonColor
: StreamChatTheme.of(context).channelTheme.actionButtonIdleColor, : StreamChatTheme.of(context)
.messageInputTheme
.actionButtonIdleColor,
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -1961,7 +1968,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildIdleSendButton(BuildContext context) { Widget _buildIdleSendButton(BuildContext context) {
return StreamSvgIcon( return StreamSvgIcon(
assetName: _getIdleSendIcon(), assetName: _getIdleSendIcon(),
color: StreamChatTheme.of(context).channelTheme.sendButtonIdleColor, color: StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor,
); );
} }
@@ -1976,7 +1983,7 @@ class MessageInputState extends State<MessageInput> {
), ),
icon: StreamSvgIcon( icon: StreamSvgIcon(
assetName: _getSendIcon(), assetName: _getSendIcon(),
color: StreamChatTheme.of(context).channelTheme.sendButtonColor, color: StreamChatTheme.of(context).messageInputTheme.sendButtonColor,
), ),
); );
} }
@@ -2008,6 +2015,7 @@ class MessageInputState extends State<MessageInput> {
if (_commandEnabled) { if (_commandEnabled) {
text = '/${_chosenCommand.name} ' + text; text = '/${_chosenCommand.name} ' + text;
FocusScope.of(context).unfocus();
} }
final attachments = [..._attachments.values]; final attachments = [..._attachments.values];
@@ -130,6 +130,7 @@ class MessageListView extends StatefulWidget {
this.emptyBuilder, this.emptyBuilder,
this.messageListBuilder, this.messageListBuilder,
this.errorWidgetBuilder, this.errorWidgetBuilder,
this.customAttachmentBuilders,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -203,6 +204,10 @@ class MessageListView extends StatefulWidget {
/// of a connection failure. /// of a connection failure.
final ErrorBuilder errorWidgetBuilder; final ErrorBuilder errorWidgetBuilder;
/// Attachment builders for the default message widget
/// Please change this in the [MessageWidget] if you are using a custom implementation
final Map<String, AttachmentBuilder> customAttachmentBuilders;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -798,6 +803,7 @@ class _MessageListViewState extends State<MessageListView> {
break; break;
} }
}, },
customAttachmentBuilders: widget.customAttachmentBuilders,
); );
} }
@@ -958,6 +964,7 @@ class _MessageListViewState extends State<MessageListView> {
break; break;
} }
}, },
customAttachmentBuilders: widget.customAttachmentBuilders,
); );
if (!message.isDeleted && if (!message.isDeleted &&
@@ -60,6 +60,9 @@ class StreamChatThemeData {
/// Theme of other users messages /// Theme of other users messages
final MessageTheme otherMessageTheme; final MessageTheme otherMessageTheme;
/// Theme of other users messages
final MessageInputTheme messageInputTheme;
/// The widget that will be built when the channel image is unavailable /// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage; final Widget Function(BuildContext, Channel) defaultChannelImage;
@@ -80,6 +83,7 @@ class StreamChatThemeData {
this.channelTheme, this.channelTheme,
this.otherMessageTheme, this.otherMessageTheme,
this.ownMessageTheme, this.ownMessageTheme,
this.messageInputTheme,
this.defaultChannelImage, this.defaultChannelImage,
this.defaultUserImage, this.defaultUserImage,
this.primaryIconTheme, this.primaryIconTheme,
@@ -108,6 +112,7 @@ class StreamChatThemeData {
ChannelTheme channelTheme, ChannelTheme channelTheme,
MessageTheme ownMessageTheme, MessageTheme ownMessageTheme,
MessageTheme otherMessageTheme, MessageTheme otherMessageTheme,
MessageInputTheme messageInputTheme,
Widget Function(BuildContext, Channel) defaultChannelImage, Widget Function(BuildContext, Channel) defaultChannelImage,
Widget Function(BuildContext, User) defaultUserImage, Widget Function(BuildContext, User) defaultUserImage,
IconThemeData primaryIconTheme, IconThemeData primaryIconTheme,
@@ -123,6 +128,7 @@ class StreamChatThemeData {
channelTheme: channelTheme ?? this.channelTheme, channelTheme: channelTheme ?? this.channelTheme,
ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme, ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme, otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
messageInputTheme: messageInputTheme ?? this.messageInputTheme,
reactionIcons: reactionIcons ?? this.reactionIcons, reactionIcons: reactionIcons ?? this.reactionIcons,
); );
@@ -143,6 +149,8 @@ class StreamChatThemeData {
other.ownMessageTheme, other.ownMessageTheme,
otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ?? otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ??
other.otherMessageTheme, other.otherMessageTheme,
messageInputTheme: messageInputTheme?.merge(other.messageInputTheme) ??
other.messageInputTheme,
reactionIcons: other.reactionIcons, reactionIcons: other.reactionIcons,
); );
} }
@@ -182,10 +190,6 @@ class StreamChatThemeData {
), ),
indicatorIconSize: 16.0), indicatorIconSize: 16.0),
channelTheme: ChannelTheme( channelTheme: ChannelTheme(
actionButtonColor: colorTheme.accentBlue,
actionButtonIdleColor: colorTheme.grey,
sendButtonColor: colorTheme.accentBlue,
sendButtonIdleColor: colorTheme.greyGainsboro,
channelHeaderTheme: ChannelHeaderTheme( channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -195,16 +199,12 @@ class StreamChatThemeData {
), ),
), ),
color: colorTheme.white, color: colorTheme.white,
title: TextStyle( title: textTheme.headlineBold,
fontSize: 14,
color: colorTheme.black,
),
lastMessageAt: TextStyle( lastMessageAt: TextStyle(
fontSize: 11, fontSize: 11,
color: colorTheme.black.withOpacity(.5), color: colorTheme.black.withOpacity(.5),
), ),
), ),
inputBackground: colorTheme.white.withAlpha(12),
), ),
ownMessageTheme: MessageTheme( ownMessageTheme: MessageTheme(
messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey), messageAuthor: textTheme.footnote.copyWith(color: colorTheme.grey),
@@ -248,6 +248,14 @@ class StreamChatThemeData {
), ),
), ),
), ),
messageInputTheme: MessageInputTheme(
sendAnimationDuration: Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentBlue,
actionButtonIdleColor: colorTheme.grey,
sendButtonColor: colorTheme.accentBlue,
sendButtonIdleColor: colorTheme.greyGainsboro,
inputBackground: colorTheme.white,
),
reactionIcons: [ reactionIcons: [
ReactionIcon( ReactionIcon(
type: 'love', type: 'love',
@@ -628,47 +636,16 @@ class ChannelTheme {
/// Theme of the [ChannelHeader] widget /// Theme of the [ChannelHeader] widget
final ChannelHeaderTheme channelHeaderTheme; final ChannelHeaderTheme channelHeaderTheme;
/// Background color of [MessageInput] send button
final Color sendButtonColor;
/// Background color of [MessageInput] action buttons
final Color actionButtonColor;
/// Background color of [MessageInput] send button
final Color sendButtonIdleColor;
/// Background color of [MessageInput] action buttons
final Color actionButtonIdleColor;
/// Background color of [MessageInput]
final Color inputBackground;
ChannelTheme({ ChannelTheme({
this.channelHeaderTheme, this.channelHeaderTheme,
this.actionButtonColor,
this.sendButtonColor,
this.actionButtonIdleColor,
this.sendButtonIdleColor,
this.inputBackground,
}); });
/// Creates a copy of [ChannelTheme] with specified attributes overridden. /// Creates a copy of [ChannelTheme] with specified attributes overridden.
ChannelTheme copyWith({ ChannelTheme copyWith({
ChannelHeaderTheme channelHeaderTheme, ChannelHeaderTheme channelHeaderTheme,
Color inputBackground,
Color actionButtonColor,
Color sendButtonColor,
Color actionButtonIdleColor,
Color sendButtonIdleColor,
}) => }) =>
ChannelTheme( ChannelTheme(
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
inputBackground: inputBackground ?? this.inputBackground,
actionButtonColor: actionButtonColor ?? this.actionButtonColor,
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
actionButtonIdleColor:
actionButtonIdleColor ?? this.actionButtonIdleColor,
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
); );
ChannelTheme merge(ChannelTheme other) { ChannelTheme merge(ChannelTheme other) {
@@ -676,11 +653,6 @@ class ChannelTheme {
return copyWith( return copyWith(
channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ?? channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ??
other.channelHeaderTheme, other.channelHeaderTheme,
inputBackground: other.inputBackground,
actionButtonColor: other.actionButtonColor,
actionButtonIdleColor: other.actionButtonIdleColor,
sendButtonColor: other.sendButtonColor,
sendButtonIdleColor: other.sendButtonIdleColor,
); );
} }
} }
@@ -873,6 +845,70 @@ class ChannelHeaderTheme {
} }
} }
/// Defines the theme dedicated to the [MessageInput] widget
class MessageInputTheme {
/// Duration of the [MessageInput] send button animation
final Duration sendAnimationDuration;
/// Background color of [MessageInput] send button
final Color sendButtonColor;
/// Background color of [MessageInput] action buttons
final Color actionButtonColor;
/// Background color of [MessageInput] send button
final Color sendButtonIdleColor;
/// Background color of [MessageInput] action buttons
final Color actionButtonIdleColor;
/// Background color of [MessageInput]
final Color inputBackground;
/// Returns a new [MessageInputTheme]
const MessageInputTheme({
this.sendAnimationDuration,
this.actionButtonColor,
this.sendButtonColor,
this.actionButtonIdleColor,
this.sendButtonIdleColor,
this.inputBackground,
});
/// Returns a new [MessageInputTheme] replacing some of its properties
MessageInputTheme copyWith({
Duration sendAnimationDuration,
Color inputBackground,
Color actionButtonColor,
Color sendButtonColor,
Color actionButtonIdleColor,
Color sendButtonIdleColor,
}) =>
MessageInputTheme(
sendAnimationDuration:
sendAnimationDuration ?? this.sendAnimationDuration,
inputBackground: inputBackground ?? this.inputBackground,
actionButtonColor: actionButtonColor ?? this.actionButtonColor,
sendButtonColor: sendButtonColor ?? this.sendButtonColor,
actionButtonIdleColor:
actionButtonIdleColor ?? this.actionButtonIdleColor,
sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor,
);
/// Merges [this] [MessageInputTheme] with the [other]
MessageInputTheme merge(MessageInputTheme other) {
if (other == null) return this;
return copyWith(
sendAnimationDuration: other.sendAnimationDuration,
inputBackground: other.inputBackground,
actionButtonColor: other.actionButtonColor,
actionButtonIdleColor: other.actionButtonIdleColor,
sendButtonColor: other.sendButtonColor,
sendButtonIdleColor: other.sendButtonIdleColor,
);
}
}
class Effect { class Effect {
final double sigmaX; final double sigmaX;
final double sigmaY; final double sigmaY;
+2 -2
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 1.2.0-beta version: 1.3.0-beta
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -11,7 +11,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter_core: ^1.2.0-beta stream_chat_flutter_core: ^1.3.0-beta
flutter_app_badger: ^1.1.2 flutter_app_badger: ^1.1.2
photo_view: ^0.10.3 photo_view: ^0.10.3
rxdart: ^0.25.0 rxdart: ^0.25.0
@@ -1,3 +1,8 @@
## 1.3.0-beta
* Update llc dependency
* Minor fixes
## 1.2.0-beta ## 1.2.0-beta
* Update llc dependency * Update llc dependency
+1 -1
View File
@@ -3,7 +3,7 @@
> The official Flutter core components for Stream Chat, a service for > The official Flutter core components for Stream Chat, a service for
> building chat applications. > building chat applications.
[![Pub](https://img.shields.io/pub/v/stream_chat_flutter.svg)](https://pub.dartlang.org/packages/stream_chat_flutter) [![Pub](https://img.shields.io/pub/v/stream_chat_flutter_core.svg)](https://pub.dartlang.org/packages/stream_chat_flutter_core)
![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square) ![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square)
[![Gitter](https://badges.gitter.im/GetStream/stream-chat-flutter.svg)](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/GetStream/stream-chat-flutter.svg)](https://gitter.im/GetStream/stream-chat-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
![CI](https://github.com/GetStream/stream-chat-flutter/workflows/stream_flutter_workflow/badge.svg?branch=master) ![CI](https://github.com/GetStream/stream-chat-flutter/workflows/stream_flutter_workflow/badge.svg?branch=master)
@@ -106,7 +106,7 @@ class ChannelListCore extends StatefulWidget {
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption> sort; final List<SortOption<ChannelModel>> sort;
/// Pagination parameters /// Pagination parameters
/// limit: the number of channels to return (max is 30) /// limit: the number of channels to return (max is 30)
@@ -118,8 +118,7 @@ class ChannelListCore extends StatefulWidget {
_ChannelListCoreState createState() => _ChannelListCoreState(); _ChannelListCoreState createState() => _ChannelListCoreState();
} }
class _ChannelListCoreState extends State<ChannelListCore> class _ChannelListCoreState extends State<ChannelListCore> {
with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
@@ -133,34 +132,21 @@ class _ChannelListCoreState extends State<ChannelListCore>
return StreamBuilder<List<Channel>>( return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream, stream: channelsBlocState.channelsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
var child;
if (snapshot.hasError) { if (snapshot.hasError) {
child = _buildErrorWidget( return _buildErrorWidget(snapshot, context, channelsBlocState);
snapshot,
context,
channelsBlocState,
);
} else if (!snapshot.hasData) {
child = _buildLoadingWidget();
} else {
final channels = snapshot.data;
child = widget.emptyBuilder(context);
if (channels.isNotEmpty) {
return widget.listBuilder(context, channels);
}
} }
if (!snapshot.hasData) {
return child; return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
}, },
); );
} }
Widget _buildLoadingWidget() {
return widget.loadingBuilder(context);
}
Widget _buildErrorWidget( Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot, AsyncSnapshot<List<Channel>> snapshot,
BuildContext context, BuildContext context,
@@ -173,10 +159,9 @@ class _ChannelListCoreState extends State<ChannelListCore>
return widget.errorBuilder(context, snapshot.error); return widget.errorBuilder(context, snapshot.error);
} }
void loadData() { Future<void> loadData() {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
channelsBloc.queryChannels(
filter: widget.filter, filter: widget.filter,
sortOptions: widget.sort, sortOptions: widget.sort,
paginationParams: widget.pagination, paginationParams: widget.pagination,
@@ -184,10 +169,9 @@ class _ChannelListCoreState extends State<ChannelListCore>
); );
} }
void paginateData() { Future<void> paginateData() {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
channelsBloc.queryChannels(
filter: widget.filter, filter: widget.filter,
sortOptions: widget.sort, sortOptions: widget.sort,
paginationParams: widget.pagination.copyWith( paginationParams: widget.pagination.copyWith(
@@ -197,39 +181,21 @@ class _ChannelListCoreState extends State<ChannelListCore>
); );
} }
StreamSubscription _subscription; StreamSubscription<Event> _subscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadData();
WidgetsBinding.instance.addObserver(this);
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
final client = StreamChatCore.of(context).client; final client = StreamChatCore.of(context).client;
_subscription = client _subscription = client
.on( .on(
EventType.connectionRecovered, EventType.connectionRecovered,
EventType.notificationAddedToChannel, EventType.notificationAddedToChannel,
EventType.notificationMessageNew, EventType.notificationMessageNew,
EventType.channelVisible, EventType.channelVisible,
) )
.listen((event) { .listen((event) => loadData());
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
});
if (widget.channelListController != null) { if (widget.channelListController != null) {
widget.channelListController.loadData = loadData; widget.channelListController.loadData = loadData;
@@ -246,20 +212,13 @@ class _ChannelListCoreState extends State<ChannelListCore>
widget.pagination?.toJson()?.toString() != widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() || oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) { widget.options?.toString() != oldWidget.options?.toString()) {
final channelsBloc = ChannelsBloc.of(context); loadData();
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
} }
} }
@override @override
void dispose() { void dispose() {
_subscription.cancel(); _subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
} }
@@ -268,10 +227,10 @@ class _ChannelListCoreState extends State<ChannelListCore>
class ChannelListController { class ChannelListController {
/// This function calls Stream's servers to load a list of channels. If there is existing data, /// This function calls Stream's servers to load a list of channels. If there is existing data,
/// calling this function causes a reload. /// calling this function causes a reload.
VoidCallback loadData; AsyncCallback loadData;
/// This function is used to load another page of data. Note, [loadData] should be /// This function is used to load another page of data. Note, [loadData] should be
/// used to populate the initial page of data. Calling [paginateData] performs a query /// used to populate the initial page of data. Calling [paginateData] performs a query
/// to load subsequent pages. /// to load subsequent pages.
VoidCallback paginateData; AsyncCallback paginateData;
} }
@@ -84,7 +84,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream /// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({ Future<void> queryChannels({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sortOptions, List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams, PaginationParams paginationParams,
Map<String, dynamic> options, Map<String, dynamic> options,
bool onlyOffline = false, bool onlyOffline = false,
@@ -102,21 +102,21 @@ class ChannelsBlocState extends State<ChannelsBloc>
paginationParams.offset == null || paginationParams.offset == null ||
paginationParams.offset == 0; paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []); final oldChannels = List<Channel>.from(channels ?? []);
final _channels = await client.queryChannels( await for (final channels in client.queryChannels(
filter: filter, filter: filter,
sort: sortOptions, sort: sortOptions,
options: options, options: options,
paginationParams: paginationParams, paginationParams: paginationParams,
onlyOffline: onlyOffline, preferOffline: onlyOffline,
); )) {
if (clear) {
if (clear) { _channelsController.add(channels);
_channelsController.add(_channels); } else {
} else { final l = oldChannels + channels;
final l = oldChannels + _channels; _channelsController.add(l);
_channelsController.add(l); }
_queryChannelsLoadingController.sink.add(false);
} }
_queryChannelsLoadingController.sink.add(false);
} catch (err, stackTrace) { } catch (err, stackTrace) {
print(err); print(err);
print(stackTrace); print(stackTrace);
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 1.2.0-beta version: 1.3.0-beta
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -10,7 +10,7 @@ environment:
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
stream_chat: ^1.2.0-beta stream_chat: ^1.3.0-beta
flutter: flutter:
sdk: flutter sdk: flutter
rxdart: ^0.25.0 rxdart: ^0.25.0
@@ -1,3 +1,7 @@
## 1.3.0-beta
* Update llc dependency
## 1.2.0-beta ## 1.2.0-beta
* Update llc dependency * Update llc dependency
@@ -0,0 +1,563 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
0E9B23A7BA08E142FA5000CF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D76F8024ABE1070895D659BA /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3F6A054EEDAF06BCF649C130 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
6FE1ECA061EBB001F6BCE8B7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D76F8024ABE1070895D659BA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EC2A45E9198C1011BED23834 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0E9B23A7BA08E142FA5000CF /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
04AAB960E493BD92262BBF82 /* Frameworks */ = {
isa = PBXGroup;
children = (
D76F8024ABE1070895D659BA /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
8559384DCD98ED6067CEF8CB /* Pods */ = {
isa = PBXGroup;
children = (
3F6A054EEDAF06BCF649C130 /* Pods-Runner.debug.xcconfig */,
EC2A45E9198C1011BED23834 /* Pods-Runner.release.xcconfig */,
6FE1ECA061EBB001F6BCE8B7 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
8559384DCD98ED6067CEF8CB /* Pods */,
04AAB960E493BD92262BBF82 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
C1EE41B94EADE099F7AF3A1C /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
BD38DACC9A0AD429D9ED9939 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BD38DACC9A0AD429D9ED9939 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
C1EE41B94EADE099F7AF3A1C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -4,4 +4,7 @@
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "group:Runner.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace> </Workspace>
@@ -11,7 +11,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
stream_chat: ^1.1.0-beta stream_chat: ^1.3.0
stream_chat_persistence: stream_chat_persistence:
path: ../ path: ../
@@ -15,9 +15,7 @@ part 'channel_query_dao.g.dart';
class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase> class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
with _$ChannelQueryDaoMixin { with _$ChannelQueryDaoMixin {
/// Creates a new channel query dao instance /// Creates a new channel query dao instance
ChannelQueryDao(this._db) : super(_db); ChannelQueryDao(MoorChatDatabase db) : super(db);
final MoorChatDatabase _db;
String _computeHash(Map<String, dynamic> filter) { String _computeHash(Map<String, dynamic> filter) {
if (filter == null) { if (filter == null) {
@@ -37,19 +35,19 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
) async { ) async {
final hash = _computeHash(filter); final hash = _computeHash(filter);
if (clearQueryCache) { if (clearQueryCache) {
await (delete(channelQueries) await batch((it) {
..where((query) => query.queryHash.equals(hash))) it.deleteWhere<ChannelQueries, ChannelQueryEntity>(
.go(); channelQueries,
(c) => c.queryHash.equals(hash),
);
});
} }
return batch((batch) { return batch((it) {
batch.insertAll( it.insertAll(
channelQueries, channelQueries,
cids.map((cid) { cids.map((cid) {
return ChannelQueryEntity( return ChannelQueryEntity(queryHash: hash, channelCid: cid);
queryHash: hash,
channelCid: cid,
);
}).toList(), }).toList(),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
@@ -57,68 +55,74 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
} }
/// Get list of channels by filter, sort and paginationParams /// Get list of channels by filter, sort and paginationParams
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelModel>> getChannels({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}) async { }) async {
assert(() {
if (sort != null && sort.any((it) => it.comparator == null)) {
throw ArgumentError(
'SortOption requires a comparator in order to sort',
);
}
return true;
}());
final hash = _computeHash(filter); final hash = _computeHash(filter);
final cachedChannels = await Future.wait(await (select(channelQueries) final cachedChannelCids = await (select(channelQueries)
..where((c) => c.queryHash.equals(hash))) ..where((c) => c.queryHash.equals(hash)))
.get() .map((c) => c.channelCid)
.then((channelQueries) { .get();
final cids = channelQueries.map((c) => c.channelCid).toList();
final query = select(channels)..where((c) => c.cid.isIn(cids));
sort = sort final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids));
?.where((s) => ChannelModel.topLevelFields.contains(s.field))
?.toList();
if (sort != null && sort.isNotEmpty) { final cachedChannels = await (query.join([
query.orderBy(sort.map((s) { leftOuterJoin(users, channels.createdById.equalsExp(users.id)),
final orderExpression = CustomExpression('channels.${s.field}'); ]).map((row) {
return (c) => OrderingTerm( final createdByEntity = row.readTable(users);
expression: orderExpression, final channelEntity = row.readTable(channels);
mode: s.direction == 1 ? OrderingMode.asc : OrderingMode.desc, return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser());
); })).get();
}).toList());
}
if (paginationParams != null) { final possibleSortingFields = cachedChannels.fold<List<String>>(
query.limit( ChannelModel.topLevelFields, (previousValue, element) {
paginationParams.limit ?? 10, return {...previousValue, ...element.extraData.keys}.toList();
offset: paginationParams.offset, });
);
}
return query.join([ sort = sort
leftOuterJoin(users, channels.createdById.equalsExp(users.id)), ?.where((s) => possibleSortingFields.contains(s.field))
]).map((row) async { ?.toList(growable: false);
final userEntity = row.readTable(users);
final channelEntity = row.readTable(channels);
final cid = channelEntity.cid; Comparator<ChannelModel> chainedComparator = (a, b) {
final members = await _db.memberDao.getMembersByCid(cid); final dateA = a.lastMessageAt ?? a.createdAt;
final reads = await _db.readDao.getReadsByCid(cid); final dateB = b.lastMessageAt ?? b.createdAt;
final messages = await _db.messageDao.getMessagesByCid(cid); return dateB.compareTo(dateA);
};
return channelEntity.toChannelState( if (sort != null && sort.isNotEmpty) {
createdBy: userEntity?.toUser(), chainedComparator = (a, b) {
members: members, int result;
reads: reads, for (final comparator in sort.map((it) => it.comparator)) {
messages: messages, try {
); result = comparator(a, b);
}).get(); } catch (e) {
})); result = 0;
}
if (result != 0) return result;
}
return 0;
};
}
if (sort?.isEmpty != false && cachedChannels?.isNotEmpty == true) { cachedChannels.sort(chainedComparator);
cachedChannels
.sort((a, b) => b.channel.updatedAt.compareTo(a.channel.updatedAt)); if (paginationParams?.offset != null) {
cachedChannels.sort((a, b) { cachedChannels.removeRange(0, paginationParams.offset);
final dateA = a.channel.lastMessageAt ?? a.channel.createdAt; }
final dateB = b.channel.lastMessageAt ?? b.channel.createdAt;
return dateB.compareTo(dateA); if (paginationParams?.limit != null) {
}); return cachedChannels.take(paginationParams.limit).toList();
} }
return cachedChannels; return cachedChannels;
@@ -1,6 +1,7 @@
export 'user_dao.dart'; export 'user_dao.dart';
export 'channel_dao.dart'; export 'channel_dao.dart';
export 'message_dao.dart'; export 'message_dao.dart';
export 'pinned_message_dao.dart';
export 'member_dao.dart'; export 'member_dao.dart';
export 'connection_event_dao.dart'; export 'connection_event_dao.dart';
export 'reaction_dao.dart'; export 'reaction_dao.dart';
@@ -17,6 +17,10 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
final MoorChatDatabase _db; final MoorChatDatabase _db;
$UsersTable get _users => alias(users, 'users');
$UsersTable get _pinnedByUsers => alias(users, 'pinnedByUsers');
/// Removes all the messages by matching [Messages.id] in [messageIds] /// Removes all the messages by matching [Messages.id] in [messageIds]
/// ///
/// This will automatically delete the following linked records /// This will automatically delete the following linked records
@@ -34,7 +38,8 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
} }
Future<Message> _messageFromJoinRow(TypedResult rows) async { Future<Message> _messageFromJoinRow(TypedResult rows) async {
final userEntity = rows.readTable(users); final userEntity = rows.readTable(_users);
final pinnedByEntity = rows.readTable(_pinnedByUsers);
final msgEntity = rows.readTable(messages); final msgEntity = rows.readTable(messages);
final latestReactions = await _db.reactionDao.getReactions(msgEntity.id); final latestReactions = await _db.reactionDao.getReactions(msgEntity.id);
final ownReactions = await _db.reactionDao.getReactionsByUserId( final ownReactions = await _db.reactionDao.getReactionsByUserId(
@@ -47,6 +52,7 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
} }
return msgEntity.toMessage( return msgEntity.toMessage(
user: userEntity?.toUser(), user: userEntity?.toUser(),
pinnedBy: pinnedByEntity?.toUser(),
latestReactions: latestReactions, latestReactions: latestReactions,
ownReactions: ownReactions, ownReactions: ownReactions,
quotedMessage: quotedMessage, quotedMessage: quotedMessage,
@@ -56,7 +62,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
/// Returns a single message by matching the [Messages.id] with [id] /// Returns a single message by matching the [Messages.id] with [id]
Future<Message> getMessageById(String id) async { Future<Message> getMessageById(String id) async {
return await (select(messages).join([ return await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(messages.id.equals(id))) ..where(messages.id.equals(id)))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
@@ -67,7 +75,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
/// [Messages.channelCid] with [cid] /// [Messages.channelCid] with [cid]
Future<List<Message>> getThreadMessages(String cid) async { Future<List<Message>> getThreadMessages(String cid) async {
return Future.wait(await (select(messages).join([ return Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(users.id)), leftOuterJoin(users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(messages.channelCid.equals(cid)) ..where(messages.channelCid.equals(cid))
..where(isNotNull(messages.parentId)) ..where(isNotNull(messages.parentId))
@@ -83,7 +93,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
PaginationParams options, PaginationParams options,
}) async { }) async {
final msgList = await Future.wait(await (select(messages).join([ final msgList = await Future.wait(await (select(messages).join([
innerJoin(users, messages.userId.equalsExp(users.id)), innerJoin(_users, messages.userId.equalsExp(_users.id)),
innerJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(messages.parentId.equals(parentId)) ..where(messages.parentId.equals(parentId))
..orderBy([OrderingTerm.asc(messages.createdAt)])) ..orderBy([OrderingTerm.asc(messages.createdAt)]))
@@ -104,7 +116,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
PaginationParams messagePagination, PaginationParams messagePagination,
}) async { }) async {
final msgList = await Future.wait(await (select(messages).join([ final msgList = await Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(messages.channelCid.equals(cid)) ..where(messages.channelCid.equals(cid))
..where( ..where(
@@ -0,0 +1,169 @@
import 'package:moor/moor.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/pinned_messages.dart';
import 'package:stream_chat_persistence/src/entity/users.dart';
import '../mapper/mapper.dart';
part 'pinned_message_dao.g.dart';
/// The Data Access Object for operations in [Messages] table.
@UseDao(tables: [PinnedMessages, Users])
class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
with _$PinnedMessageDaoMixin {
/// Creates a new message dao instance
PinnedMessageDao(this._db) : super(_db);
final MoorChatDatabase _db;
$UsersTable get _users => alias(users, 'users');
$UsersTable get _pinnedByUsers => alias(users, 'pinnedByUsers');
/// Removes all the messages by matching [PinnedMessages.id] in [messageIds]
///
/// This will automatically delete the following linked records
/// 1. Message Reactions
Future<void> deleteMessageByIds(List<String> messageIds) {
return (delete(pinnedMessages)..where((tbl) => tbl.id.isIn(messageIds)))
.go();
}
/// Removes all the messages by matching [PinnedMessages.channelCid] in [cids]
///
/// This will automatically delete the following linked records
/// 1. Message Reactions
Future<void> deleteMessageByCids(List<String> cids) async {
return (delete(pinnedMessages)..where((tbl) => tbl.channelCid.isIn(cids)))
.go();
}
Future<Message> _messageFromJoinRow(TypedResult rows) async {
final userEntity = rows.readTable(users);
final pinnedByEntity = rows.readTable(_pinnedByUsers);
final msgEntity = rows.readTable(pinnedMessages);
final latestReactions = await _db.reactionDao.getReactions(msgEntity.id);
final ownReactions = await _db.reactionDao.getReactionsByUserId(
msgEntity.id,
_db.userId,
);
Message quotedMessage;
if (msgEntity.quotedMessageId != null) {
quotedMessage = await getMessageById(msgEntity.quotedMessageId);
}
return msgEntity.toMessage(
user: userEntity?.toUser(),
pinnedBy: pinnedByEntity?.toUser(),
latestReactions: latestReactions,
ownReactions: ownReactions,
quotedMessage: quotedMessage,
);
}
/// Returns a single message by matching the [PinnedMessages.id] with [id]
Future<Message> getMessageById(String id) async {
return await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
..where(pinnedMessages.id.equals(id)))
.map(_messageFromJoinRow)
.getSingle();
}
/// Returns all the messages of a particular thread by matching
/// [PinnedMessages.channelCid] with [cid]
Future<List<Message>> getThreadMessages(String cid) async {
return Future.wait(await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
..where(pinnedMessages.channelCid.equals(cid))
..where(isNotNull(pinnedMessages.parentId))
..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]))
.map(_messageFromJoinRow)
.get());
}
/// Returns all the messages of a particular thread by matching
/// [PinnedMessages.parentId] with [parentId]
Future<List<Message>> getThreadMessagesByParentId(
String parentId, {
PaginationParams options,
}) async {
final msgList = await Future.wait(await (select(pinnedMessages).join([
innerJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
innerJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
..where(pinnedMessages.parentId.equals(parentId))
..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]))
.map(_messageFromJoinRow)
.get());
if (options?.lessThan != null) {
final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan);
msgList.removeRange(lessThanIndex, msgList.length);
}
return msgList;
}
/// Returns all the messages of a channel by matching
/// [PinnedMessages.channelCid] with [parentId]
Future<List<Message>> getMessagesByCid(
String cid, {
PaginationParams messagePagination,
}) async {
final msgList = await Future.wait(await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
])
..where(pinnedMessages.channelCid.equals(cid))
..where(isNull(pinnedMessages.parentId) |
pinnedMessages.showInChannel.equals(true))
..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]))
.map(_messageFromJoinRow)
.get());
if (messagePagination?.lessThan != null) {
final lessThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.lessThan,
);
if (lessThanIndex != -1) {
msgList.removeRange(lessThanIndex, msgList.length);
}
}
if (messagePagination?.greaterThanOrEqual != null) {
final greaterThanIndex = msgList.indexWhere(
(m) => m.id == messagePagination.greaterThanOrEqual,
);
if (greaterThanIndex != -1) {
msgList.removeRange(0, greaterThanIndex);
}
}
if (messagePagination?.limit != null) {
return msgList.take(messagePagination.limit).toList();
}
return msgList;
}
/// Updates the message data of a particular channel with
/// the new [messageList] data
Future<void> updateMessages(String cid, List<Message> messageList) async {
if (messageList == null) {
return;
}
return batch((batch) {
batch.insertAll(
pinnedMessages,
messageList.map((it) => it.toPinnedEntity(cid: cid)).toList(),
mode: InsertMode.insertOrReplace,
);
});
}
}
@@ -0,0 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pinned_message_dao.dart';
// **************************************************************************
// DaoGenerator
// **************************************************************************
mixin _$PinnedMessageDaoMixin on DatabaseAccessor<MoorChatDatabase> {
$PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages;
$UsersTable get users => attachedDatabase.users;
}
@@ -25,6 +25,7 @@ LazyDatabase _openConnection(
@UseMoor(tables: [ @UseMoor(tables: [
Channels, Channels,
Messages, Messages,
PinnedMessages,
Reactions, Reactions,
Users, Users,
Members, Members,
@@ -35,6 +36,7 @@ LazyDatabase _openConnection(
UserDao, UserDao,
ChannelDao, ChannelDao,
MessageDao, MessageDao,
PinnedMessageDao,
MemberDao, MemberDao,
ReactionDao, ReactionDao,
ReadDao, ReadDao,
@@ -67,7 +69,7 @@ class MoorChatDatabase extends _$MoorChatDatabase {
// you should bump this number whenever you change or add a table definition. // you should bump this number whenever you change or add a table definition.
@override @override
int get schemaVersion => 1; int get schemaVersion => 2;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
export 'channels.dart'; export 'channels.dart';
export 'messages.dart'; export 'messages.dart';
export 'pinned_messages.dart';
export 'reactions.dart'; export 'reactions.dart';
export 'users.dart'; export 'users.dart';
export 'members.dart'; export 'members.dart';
@@ -64,6 +64,18 @@ class Messages extends Table {
/// Id of the User who sent the message /// Id of the User who sent the message
TextColumn get userId => text().nullable()(); TextColumn get userId => text().nullable()();
/// Whether the message is pinned or not
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
/// The DateTime at which the message was pinned
DateTimeColumn get pinnedAt => dateTime().nullable()();
/// The DateTime on which the message pin expires
DateTimeColumn get pinExpires => dateTime().nullable()();
/// Id of the User who pinned the message
TextColumn get pinnedByUserId => text().nullable()();
/// The channel cid of which this message is part of /// The channel cid of which this message is part of
TextColumn get channelCid => text().nullable().customConstraint( TextColumn get channelCid => text().nullable().customConstraint(
'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE')(); 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE')();
@@ -0,0 +1,7 @@
import 'package:moor/moor.dart';
import 'messages.dart';
/// Represents a [PinnedMessages] table in [MoorChatDatabase].
@DataClassName('PinnedMessageEntity')
class PinnedMessages extends Messages {}
@@ -28,11 +28,13 @@ extension ChannelEntityX on ChannelEntity {
List<Member> members, List<Member> members,
List<Read> reads, List<Read> reads,
List<Message> messages, List<Message> messages,
List<Message> pinnedMessages,
}) { }) {
return ChannelState( return ChannelState(
members: members, members: members,
read: reads, read: reads,
messages: messages, messages: messages,
pinnedMessages: pinnedMessages,
channel: toChannelModel(createdBy: createdBy), channel: toChannelModel(createdBy: createdBy),
); );
} }
@@ -5,3 +5,4 @@ export 'event_mapper.dart';
export 'member_mapper.dart'; export 'member_mapper.dart';
export 'read_mapper.dart'; export 'read_mapper.dart';
export 'message_mapper.dart'; export 'message_mapper.dart';
export 'pinned_message_mapper.dart';
@@ -8,6 +8,7 @@ extension MessageEntityX on MessageEntity {
/// Maps a [MessageEntity] into [Message] /// Maps a [MessageEntity] into [Message]
Message toMessage({ Message toMessage({
User user, User user,
User pinnedBy,
List<Reaction> latestReactions, List<Reaction> latestReactions,
List<Reaction> ownReactions, List<Reaction> ownReactions,
Message quotedMessage, Message quotedMessage,
@@ -37,6 +38,10 @@ extension MessageEntityX on MessageEntity {
text: messageText, text: messageText,
user: user, user: user,
deletedAt: deletedAt, deletedAt: deletedAt,
pinned: pinned,
pinnedAt: pinnedAt,
pinExpires: pinExpires,
pinnedBy: pinnedBy,
); );
} }
} }
@@ -67,6 +72,10 @@ extension MessageX on Message {
userId: user?.id, userId: user?.id,
deletedAt: deletedAt, deletedAt: deletedAt,
messageText: text, messageText: text,
pinned: pinned,
pinnedAt: pinnedAt,
pinExpires: pinExpires,
pinnedByUserId: pinnedBy?.id,
); );
} }
} }
@@ -0,0 +1,81 @@
import 'dart:convert';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [PinnedMessageEntity]
extension PinnedMessageEntityX on PinnedMessageEntity {
/// Maps a [PinnedMessageEntity] into [Message]
Message toMessage({
User user,
User pinnedBy,
List<Reaction> latestReactions,
List<Reaction> ownReactions,
Message quotedMessage,
}) {
return Message(
shadowed: shadowed,
latestReactions: latestReactions,
ownReactions: ownReactions,
attachments: attachments?.map((it) {
final json = jsonDecode(it);
return Attachment.fromData(json);
})?.toList(),
createdAt: createdAt,
extraData: extraData,
updatedAt: updatedAt,
id: id,
type: type,
status: status,
command: command,
parentId: parentId,
quotedMessageId: quotedMessageId,
quotedMessage: quotedMessage,
reactionCounts: reactionCounts,
reactionScores: reactionScores,
replyCount: replyCount,
showInChannel: showInChannel,
text: messageText,
user: user,
deletedAt: deletedAt,
pinned: pinned,
pinnedAt: pinnedAt,
pinExpires: pinExpires,
pinnedBy: pinnedBy,
);
}
}
/// Useful mapping functions for [Message]
extension PMessageX on Message {
/// Maps a [Message] into [PinnedMessageEntity]
PinnedMessageEntity toPinnedEntity({String cid}) {
return PinnedMessageEntity(
id: id,
attachments: attachments?.map((it) {
return jsonEncode(it.toData());
})?.toList(),
channelCid: cid,
type: type,
parentId: parentId,
quotedMessageId: quotedMessageId,
command: command,
createdAt: createdAt,
shadowed: shadowed,
showInChannel: showInChannel,
replyCount: replyCount,
reactionScores: reactionScores,
reactionCounts: reactionCounts,
status: status,
updatedAt: updatedAt,
extraData: extraData,
userId: user?.id,
deletedAt: deletedAt,
messageText: text,
pinned: pinned,
pinnedAt: pinnedAt,
pinExpires: pinExpires,
pinnedByUserId: pinnedBy?.id,
);
}
}
@@ -81,11 +81,21 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
return _db.messageDao.deleteMessageByIds(messageIds); return _db.messageDao.deleteMessageByIds(messageIds);
} }
@override
Future<void> deletePinnedMessageByIds(List<String> messageIds) {
return _db.pinnedMessageDao.deleteMessageByIds(messageIds);
}
@override @override
Future<void> deleteMessageByCids(List<String> cids) { Future<void> deleteMessageByCids(List<String> cids) {
return _db.messageDao.deleteMessageByCids(cids); return _db.messageDao.deleteMessageByCids(cids);
} }
@override
Future<void> deletePinnedMessageByCids(List<String> cids) {
return _db.pinnedMessageDao.deleteMessageByCids(cids);
}
@override @override
Future<List<Member>> getMembersByCid(String cid) { Future<List<Member>> getMembersByCid(String cid) {
return _db.memberDao.getMembersByCid(cid); return _db.memberDao.getMembersByCid(cid);
@@ -107,6 +117,17 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
); );
} }
@override
Future<List<Message>> getPinnedMessagesByCid(
String cid, {
PaginationParams messagePagination,
}) {
return _db.pinnedMessageDao.getMessagesByCid(
cid,
messagePagination: messagePagination,
);
}
@override @override
Future<List<Read>> getReadsByCid(String cid) { Future<List<Read>> getReadsByCid(String cid) {
return _db.readDao.getReadsByCid(cid); return _db.readDao.getReadsByCid(cid);
@@ -140,14 +161,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override @override
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}) { }) async {
return _db.channelQueryDao.getChannelStates( final channels = await _db.channelQueryDao.getChannels(
filter: filter, filter: filter,
sort: sort, sort: sort,
paginationParams: paginationParams, paginationParams: paginationParams,
); );
return Future.wait(channels.map((e) => getChannelStateByCid(e.cid)));
} }
@override @override
@@ -178,6 +200,11 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
return _db.messageDao.updateMessages(cid, messages); return _db.messageDao.updateMessages(cid, messages);
} }
@override
Future<void> updatePinnedMessages(String cid, List<Message> messages) {
return _db.pinnedMessageDao.updateMessages(cid, messages);
}
@override @override
Future<void> updateReactions(List<Reaction> reactions) { Future<void> updateReactions(List<Reaction> reactions) {
return _db.reactionDao.updateReactions(reactions); return _db.reactionDao.updateReactions(reactions);
@@ -1,7 +1,7 @@
name: stream_chat_persistence name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
version: 1.2.0-beta version: 1.3.0-beta
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -13,7 +13,7 @@ dependencies:
path: ^1.7.0 path: ^1.7.0
path_provider: ^1.6.27 path_provider: ^1.6.27
sqlite3_flutter_libs: ^0.4.0+1 sqlite3_flutter_libs: ^0.4.0+1
stream_chat: ^1.2.0-beta stream_chat: ^1.3.0-beta
dev_dependencies: dev_dependencies:
test: ^1.15.7 test: ^1.15.7