diff --git a/api/pubnub-chat.api b/api/pubnub-chat.api index 8e3d503c..d9e6f44d 100644 --- a/api/pubnub-chat.api +++ b/api/pubnub-chat.api @@ -6,7 +6,6 @@ public final class com/pubnub/chat/MediatorsKt { public static final fun streamUpdatesOn (Lcom/pubnub/chat/Channel$Companion;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public static final fun streamUpdatesOn (Lcom/pubnub/chat/Membership$Companion;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public static final fun streamUpdatesOn (Lcom/pubnub/chat/Message$Companion;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; - public static final fun streamUpdatesOn (Lcom/pubnub/chat/ThreadChannel$Companion;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public static final fun streamUpdatesOn (Lcom/pubnub/chat/ThreadMessage$Companion;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public static final fun streamUpdatesOn (Lcom/pubnub/chat/User$Companion;Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; } diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 11bd14f9..9c71f082 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -659,6 +659,22 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pubnub@8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/pubnub/-/pubnub-8.4.1.tgz#5f6f19e84d3187dc8aee0a458bd6b05e90d43e6a" + integrity sha512-mPlwVoHJDWPasZx52UfSMiPX5TATm5A+ficSogyqDqTQ4w5EQnwxH+PJdsWc0mPnlT051jM1vIISMeM0fQ30CQ== + dependencies: + agentkeepalive "^3.5.2" + buffer "^6.0.3" + cbor-js "^0.1.0" + cbor-sync "^1.0.4" + form-data "^4.0.0" + lil-uuid "^0.1.1" + node-fetch "^2.7.0" + proxy-agent "^6.3.0" + react-native-url-polyfill "^2.0.0" + text-encoding "^0.7.0" + pubnub@8.6.0: version "8.6.0" resolved "https://registry.yarnpkg.com/pubnub/-/pubnub-8.6.0.tgz#75524e7ed3653090652d160ce83ac089362a0379" diff --git a/pubnub-chat-api/api/pubnub-chat-api.api b/pubnub-chat-api/api/pubnub-chat-api.api index 67241cd5..4e0c9e9c 100644 --- a/pubnub-chat-api/api/pubnub-chat-api.api +++ b/pubnub-chat-api/api/pubnub-chat-api.api @@ -1,23 +1,21 @@ -public abstract interface class com/pubnub/chat/Channel { - public static final field Companion Lcom/pubnub/chat/Channel$Companion; +public abstract interface class com/pubnub/chat/BaseChannel { + public static final field Companion Lcom/pubnub/chat/BaseChannel$Companion; public abstract fun connect (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun delete (Z)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun delete$default (Lcom/pubnub/chat/Channel;ZILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun delete$default (Lcom/pubnub/chat/BaseChannel;ZILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun deleteFile (Ljava/lang/String;Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public abstract fun forwardMessage (Lcom/pubnub/chat/Message;)Lcom/pubnub/kmp/PNFuture; + public abstract fun forwardMessage (Lcom/pubnub/chat/BaseMessage;)Lcom/pubnub/kmp/PNFuture; public abstract fun getChat ()Lcom/pubnub/chat/Chat; public abstract fun getCustom ()Ljava/util/Map; public abstract fun getDescription ()Ljava/lang/String; public abstract fun getFiles (ILjava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun getFiles$default (Lcom/pubnub/chat/Channel;ILjava/lang/String;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun getFiles$default (Lcom/pubnub/chat/BaseChannel;ILjava/lang/String;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getHistory (Ljava/lang/Long;Ljava/lang/Long;I)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun getHistory$default (Lcom/pubnub/chat/Channel;Ljava/lang/Long;Ljava/lang/Long;IILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun getHistory$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/Long;Ljava/lang/Long;IILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getId ()Ljava/lang/String; - public abstract fun getMembers (Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/lang/String;Ljava/util/Collection;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun getMembers$default (Lcom/pubnub/chat/Channel;Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/lang/String;Ljava/util/Collection;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getMessage (J)Lcom/pubnub/kmp/PNFuture; public abstract fun getMessageReportsHistory (Ljava/lang/Long;Ljava/lang/Long;I)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun getMessageReportsHistory$default (Lcom/pubnub/chat/Channel;Ljava/lang/Long;Ljava/lang/Long;IILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun getMessageReportsHistory$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/Long;Ljava/lang/Long;IILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getName ()Ljava/lang/String; public abstract fun getPinnedMessage ()Lcom/pubnub/kmp/PNFuture; public abstract fun getStatus ()Ljava/lang/String; @@ -26,37 +24,88 @@ public abstract interface class com/pubnub/chat/Channel { public abstract fun getUpdated ()Ljava/lang/String; public abstract fun getUserRestrictions (Lcom/pubnub/chat/User;)Lcom/pubnub/kmp/PNFuture; public abstract fun getUserSuggestions (Ljava/lang/String;I)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun getUserSuggestions$default (Lcom/pubnub/chat/Channel;Ljava/lang/String;IILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun getUserSuggestions$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/String;IILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getUsersRestrictions (Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/util/Collection;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun getUsersRestrictions$default (Lcom/pubnub/chat/Channel;Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/util/Collection;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; - public abstract fun invite (Lcom/pubnub/chat/User;)Lcom/pubnub/kmp/PNFuture; - public abstract fun inviteMultiple (Ljava/util/Collection;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun getUsersRestrictions$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/util/Collection;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun isPresent (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public abstract fun join (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun join$default (Lcom/pubnub/chat/Channel;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; - public abstract fun leave ()Lcom/pubnub/kmp/PNFuture; - public abstract fun pinMessage (Lcom/pubnub/chat/Message;)Lcom/pubnub/kmp/PNFuture; - public abstract fun plus (Lcom/pubnub/api/models/consumer/objects/channel/PNChannelMetadata;)Lcom/pubnub/chat/Channel; + public abstract fun pinMessage (Lcom/pubnub/chat/BaseMessage;)Lcom/pubnub/kmp/PNFuture; + public abstract fun plus (Lcom/pubnub/api/models/consumer/objects/channel/PNChannelMetadata;)Lcom/pubnub/chat/BaseChannel; public abstract fun registerForPush ()Lcom/pubnub/kmp/PNFuture; - public abstract fun sendText (Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Lcom/pubnub/chat/Message;Ljava/util/List;Ljava/util/Collection;Ljava/util/Map;)Lcom/pubnub/kmp/PNFuture; - public abstract fun sendText (Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Lcom/pubnub/chat/Message;Ljava/util/List;Ljava/util/Map;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun sendText$default (Lcom/pubnub/chat/Channel;Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Lcom/pubnub/chat/Message;Ljava/util/List;Ljava/util/Collection;Ljava/util/Map;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun sendText$default (Lcom/pubnub/chat/Channel;Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Lcom/pubnub/chat/Message;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public abstract fun sendText (Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Lcom/pubnub/chat/BaseMessage;Ljava/util/List;Ljava/util/Collection;Ljava/util/Map;)Lcom/pubnub/kmp/PNFuture; + public abstract fun sendText (Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Lcom/pubnub/chat/BaseMessage;Ljava/util/List;Ljava/util/Map;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun sendText$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Lcom/pubnub/chat/BaseMessage;Ljava/util/List;Ljava/util/Collection;Ljava/util/Map;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun sendText$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/String;Ljava/util/Map;ZZLjava/lang/Integer;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Lcom/pubnub/chat/BaseMessage;Ljava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun setRestrictions (Lcom/pubnub/chat/User;ZZLjava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun setRestrictions$default (Lcom/pubnub/chat/Channel;Lcom/pubnub/chat/User;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun setRestrictions$default (Lcom/pubnub/chat/BaseChannel;Lcom/pubnub/chat/User;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun startTyping ()Lcom/pubnub/kmp/PNFuture; public abstract fun stopTyping ()Lcom/pubnub/kmp/PNFuture; public abstract fun streamMessageReports (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun streamPresence (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; - public abstract fun streamReadReceipts (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun unpinMessage ()Lcom/pubnub/kmp/PNFuture; public abstract fun unregisterFromPush ()Lcom/pubnub/kmp/PNFuture; public abstract fun update (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lcom/pubnub/chat/types/ChannelType;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun update$default (Lcom/pubnub/chat/Channel;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lcom/pubnub/chat/types/ChannelType;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun update$default (Lcom/pubnub/chat/BaseChannel;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lcom/pubnub/chat/types/ChannelType;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun whoIsPresent ()Lcom/pubnub/kmp/PNFuture; } +public final class com/pubnub/chat/BaseChannel$Companion { +} + +public abstract interface class com/pubnub/chat/BaseMessage { + public static final field Companion Lcom/pubnub/chat/BaseMessage$Companion; + public abstract fun delete (ZZ)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun delete$default (Lcom/pubnub/chat/BaseMessage;ZZILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public abstract fun editText (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; + public abstract fun forward (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; + public abstract fun getActions ()Ljava/util/Map; + public abstract fun getChannelId ()Ljava/lang/String; + public abstract fun getChat ()Lcom/pubnub/chat/Chat; + public abstract fun getContent ()Lcom/pubnub/chat/types/EventContent$TextMessageContent; + public abstract fun getDeleted ()Z + public abstract fun getError ()Lcom/pubnub/api/PubNubError; + public abstract fun getFiles ()Ljava/util/List; + public abstract fun getMentionedUsers ()Ljava/util/Map; + public abstract fun getMeta ()Ljava/util/Map; + public abstract fun getQuotedMessage ()Lcom/pubnub/chat/types/QuotedMessage; + public abstract fun getReactions ()Ljava/util/Map; + public abstract fun getReferencedChannels ()Ljava/util/Map; + public abstract fun getText ()Ljava/lang/String; + public abstract fun getTextLinks ()Ljava/util/List; + public abstract fun getTimetoken ()J + public abstract fun getType ()Ljava/lang/String; + public abstract fun getUserId ()Ljava/lang/String; + public abstract fun hasUserReaction (Ljava/lang/String;)Z + public abstract fun pin ()Lcom/pubnub/kmp/PNFuture; + public abstract fun report (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; + public abstract fun restore ()Lcom/pubnub/kmp/PNFuture; + public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; + public abstract fun toggleReaction (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; +} + +public final class com/pubnub/chat/BaseMessage$Companion { +} + +public abstract interface class com/pubnub/chat/Channel : com/pubnub/chat/BaseChannel { + public static final field Companion Lcom/pubnub/chat/Channel$Companion; + public abstract fun connect (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; + public abstract fun delete (Z)Lcom/pubnub/kmp/PNFuture; + public abstract fun getHistory (Ljava/lang/Long;Ljava/lang/Long;I)Lcom/pubnub/kmp/PNFuture; + public abstract fun getMembers (Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/lang/String;Ljava/util/Collection;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun getMembers$default (Lcom/pubnub/chat/Channel;Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/lang/String;Ljava/util/Collection;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public abstract fun getMessage (J)Lcom/pubnub/kmp/PNFuture; + public abstract fun invite (Lcom/pubnub/chat/User;)Lcom/pubnub/kmp/PNFuture; + public abstract fun inviteMultiple (Ljava/util/Collection;)Lcom/pubnub/kmp/PNFuture; + public abstract fun join (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun join$default (Lcom/pubnub/chat/Channel;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public abstract fun leave ()Lcom/pubnub/kmp/PNFuture; + public abstract fun plus (Lcom/pubnub/api/models/consumer/objects/channel/PNChannelMetadata;)Lcom/pubnub/chat/Channel; + public abstract fun streamReadReceipts (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; + public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; + public abstract fun unpinMessage ()Lcom/pubnub/kmp/PNFuture; + public abstract fun update (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lcom/pubnub/chat/types/ChannelType;)Lcom/pubnub/kmp/PNFuture; +} + public final class com/pubnub/chat/Channel$Companion { } @@ -135,7 +184,7 @@ public abstract interface class com/pubnub/chat/Membership { public abstract fun getUpdated ()Ljava/lang/String; public abstract fun getUser ()Lcom/pubnub/chat/User; public abstract fun plus (Lcom/pubnub/api/models/consumer/pubsub/objects/PNSetMembershipEvent;)Lcom/pubnub/chat/Membership; - public abstract fun setLastReadMessage (Lcom/pubnub/chat/Message;)Lcom/pubnub/kmp/PNFuture; + public abstract fun setLastReadMessage (Lcom/pubnub/chat/BaseMessage;)Lcom/pubnub/kmp/PNFuture; public abstract fun setLastReadMessageTimetoken (J)Lcom/pubnub/kmp/PNFuture; public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun update (Ljava/lang/Object;)Lcom/pubnub/kmp/PNFuture; @@ -180,36 +229,13 @@ public final class com/pubnub/chat/MentionTarget$User : com/pubnub/chat/MentionT public fun toString ()Ljava/lang/String; } -public abstract interface class com/pubnub/chat/Message { +public abstract interface class com/pubnub/chat/Message : com/pubnub/chat/BaseMessage { public static final field Companion Lcom/pubnub/chat/Message$Companion; public abstract fun createThread ()Lcom/pubnub/kmp/PNFuture; - public abstract fun delete (ZZ)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun delete$default (Lcom/pubnub/chat/Message;ZZILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; - public abstract fun editText (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public abstract fun forward (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public abstract fun getActions ()Ljava/util/Map; - public abstract fun getChannelId ()Ljava/lang/String; - public abstract fun getChat ()Lcom/pubnub/chat/Chat; - public abstract fun getContent ()Lcom/pubnub/chat/types/EventContent$TextMessageContent; - public abstract fun getDeleted ()Z - public abstract fun getError ()Lcom/pubnub/api/PubNubError; - public abstract fun getFiles ()Ljava/util/List; public abstract fun getHasThread ()Z - public abstract fun getMentionedUsers ()Ljava/util/Map; - public abstract fun getMeta ()Ljava/util/Map; - public abstract fun getQuotedMessage ()Lcom/pubnub/chat/types/QuotedMessage; - public abstract fun getReactions ()Ljava/util/Map; - public abstract fun getReferencedChannels ()Ljava/util/Map; - public abstract fun getText ()Ljava/lang/String; - public abstract fun getTextLinks ()Ljava/util/List; public abstract fun getThread ()Lcom/pubnub/kmp/PNFuture; - public abstract fun getTimetoken ()J - public abstract fun getType ()Ljava/lang/String; - public abstract fun getUserId ()Ljava/lang/String; - public abstract fun hasUserReaction (Ljava/lang/String;)Z public abstract fun pin ()Lcom/pubnub/kmp/PNFuture; public abstract fun removeThread ()Lcom/pubnub/kmp/PNFuture; - public abstract fun report (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; public abstract fun restore ()Lcom/pubnub/kmp/PNFuture; public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun toggleReaction (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; @@ -221,10 +247,10 @@ public final class com/pubnub/chat/Message$Companion { public abstract interface class com/pubnub/chat/MessageDraft { public abstract fun addChangeListener (Lcom/pubnub/chat/MessageDraftChangeListener;)V public abstract fun addMention (IILcom/pubnub/chat/MentionTarget;)V - public abstract fun getChannel ()Lcom/pubnub/chat/Channel; + public abstract fun getChannel ()Lcom/pubnub/chat/BaseChannel; public abstract fun getChannelLimit ()I public abstract fun getFiles ()Ljava/util/List; - public abstract fun getQuotedMessage ()Lcom/pubnub/chat/Message; + public abstract fun getQuotedMessage ()Lcom/pubnub/chat/BaseMessage; public abstract fun getUserLimit ()I public abstract fun getUserSuggestionSource ()Lcom/pubnub/chat/MessageDraft$UserSuggestionSource; public abstract fun insertSuggestedMention (Lcom/pubnub/chat/SuggestedMention;Ljava/lang/String;)V @@ -235,7 +261,7 @@ public abstract interface class com/pubnub/chat/MessageDraft { public abstract fun removeText (II)V public abstract fun send (Ljava/util/Map;ZZLjava/lang/Integer;)Lcom/pubnub/kmp/PNFuture; public static synthetic fun send$default (Lcom/pubnub/chat/MessageDraft;Ljava/util/Map;ZZLjava/lang/Integer;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; - public abstract fun setQuotedMessage (Lcom/pubnub/chat/Message;)V + public abstract fun setQuotedMessage (Lcom/pubnub/chat/BaseMessage;)V public abstract fun update (Ljava/lang/String;)V } @@ -295,12 +321,12 @@ public final class com/pubnub/chat/SuggestedMention { public final fun getTarget ()Lcom/pubnub/chat/MentionTarget; } -public abstract interface class com/pubnub/chat/ThreadChannel : com/pubnub/chat/Channel { +public abstract interface class com/pubnub/chat/ThreadChannel : com/pubnub/chat/BaseChannel { public static final field Companion Lcom/pubnub/chat/ThreadChannel$Companion; public abstract fun getHistory (Ljava/lang/Long;Ljava/lang/Long;I)Lcom/pubnub/kmp/PNFuture; public abstract fun getParentChannelId ()Ljava/lang/String; public abstract fun getParentMessage ()Lcom/pubnub/chat/Message; - public abstract fun pinMessage (Lcom/pubnub/chat/Message;)Lcom/pubnub/kmp/PNFuture; + public abstract fun pinMessage (Lcom/pubnub/chat/BaseMessage;)Lcom/pubnub/kmp/PNFuture; public abstract fun pinMessageToParentChannel (Lcom/pubnub/chat/ThreadMessage;)Lcom/pubnub/kmp/PNFuture; public abstract fun unpinMessage ()Lcom/pubnub/kmp/PNFuture; public abstract fun unpinMessageFromParentChannel ()Lcom/pubnub/kmp/PNFuture; @@ -309,10 +335,14 @@ public abstract interface class com/pubnub/chat/ThreadChannel : com/pubnub/chat/ public final class com/pubnub/chat/ThreadChannel$Companion { } -public abstract interface class com/pubnub/chat/ThreadMessage : com/pubnub/chat/Message { +public abstract interface class com/pubnub/chat/ThreadMessage : com/pubnub/chat/BaseMessage { public static final field Companion Lcom/pubnub/chat/ThreadMessage$Companion; public abstract fun getParentChannelId ()Ljava/lang/String; + public abstract fun pin ()Lcom/pubnub/kmp/PNFuture; public abstract fun pinToParentChannel ()Lcom/pubnub/kmp/PNFuture; + public abstract fun restore ()Lcom/pubnub/kmp/PNFuture; + public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; + public abstract fun toggleReaction (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; public abstract fun unpinFromParentChannel ()Lcom/pubnub/kmp/PNFuture; } @@ -325,7 +355,7 @@ public abstract interface class com/pubnub/chat/User { public abstract fun delete (Z)Lcom/pubnub/kmp/PNFuture; public static synthetic fun delete$default (Lcom/pubnub/chat/User;ZILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getActive ()Z - public abstract fun getChannelRestrictions (Lcom/pubnub/chat/Channel;)Lcom/pubnub/kmp/PNFuture; + public abstract fun getChannelRestrictions (Lcom/pubnub/chat/BaseChannel;)Lcom/pubnub/kmp/PNFuture; public abstract fun getChannelsRestrictions (Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/util/Collection;)Lcom/pubnub/kmp/PNFuture; public static synthetic fun getChannelsRestrictions$default (Lcom/pubnub/chat/User;Ljava/lang/Integer;Lcom/pubnub/api/models/consumer/objects/PNPage;Ljava/util/Collection;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun getChat ()Lcom/pubnub/chat/Chat; @@ -344,8 +374,8 @@ public abstract interface class com/pubnub/chat/User { public abstract fun getUpdated ()Ljava/lang/String; public abstract fun isPresentOn (Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; public abstract fun plus (Lcom/pubnub/api/models/consumer/objects/uuid/PNUUIDMetadata;)Lcom/pubnub/chat/User; - public abstract fun setRestrictions (Lcom/pubnub/chat/Channel;ZZLjava/lang/String;)Lcom/pubnub/kmp/PNFuture; - public static synthetic fun setRestrictions$default (Lcom/pubnub/chat/User;Lcom/pubnub/chat/Channel;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; + public abstract fun setRestrictions (Lcom/pubnub/chat/BaseChannel;ZZLjava/lang/String;)Lcom/pubnub/kmp/PNFuture; + public static synthetic fun setRestrictions$default (Lcom/pubnub/chat/User;Lcom/pubnub/chat/BaseChannel;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/pubnub/kmp/PNFuture; public abstract fun streamUpdates (Lkotlin/jvm/functions/Function1;)Ljava/lang/AutoCloseable; public abstract fun update (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)Lcom/pubnub/kmp/PNFuture; public abstract fun update (Lkotlin/jvm/functions/Function2;)Lcom/pubnub/kmp/PNFuture; @@ -505,6 +535,7 @@ public final class com/pubnub/chat/types/ChannelMentionData : com/pubnub/chat/ty public fun (Lcom/pubnub/chat/Event;Lcom/pubnub/chat/Message;Ljava/lang/String;Ljava/lang/String;)V public final fun getChannelId ()Ljava/lang/String; public fun getEvent ()Lcom/pubnub/chat/Event; + public synthetic fun getMessage ()Lcom/pubnub/chat/BaseMessage; public fun getMessage ()Lcom/pubnub/chat/Message; public fun getUserId ()Ljava/lang/String; } @@ -928,9 +959,10 @@ public final class com/pubnub/chat/types/TextLink$Companion { } public final class com/pubnub/chat/types/ThreadMentionData : com/pubnub/chat/types/UserMentionData { - public fun (Lcom/pubnub/chat/Event;Lcom/pubnub/chat/Message;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun (Lcom/pubnub/chat/Event;Lcom/pubnub/chat/ThreadMessage;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public fun getEvent ()Lcom/pubnub/chat/Event; - public fun getMessage ()Lcom/pubnub/chat/Message; + public synthetic fun getMessage ()Lcom/pubnub/chat/BaseMessage; + public fun getMessage ()Lcom/pubnub/chat/ThreadMessage; public final fun getParentChannelId ()Ljava/lang/String; public final fun getThreadChannelId ()Ljava/lang/String; public fun getUserId ()Ljava/lang/String; @@ -938,7 +970,7 @@ public final class com/pubnub/chat/types/ThreadMentionData : com/pubnub/chat/typ public abstract class com/pubnub/chat/types/UserMentionData { public abstract fun getEvent ()Lcom/pubnub/chat/Event; - public abstract fun getMessage ()Lcom/pubnub/chat/Message; + public abstract fun getMessage ()Lcom/pubnub/chat/BaseMessage; public abstract fun getUserId ()Ljava/lang/String; } diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/BaseChannel.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/BaseChannel.kt new file mode 100644 index 00000000..d2a13308 --- /dev/null +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/BaseChannel.kt @@ -0,0 +1,476 @@ +package com.pubnub.chat + +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.models.consumer.files.PNDeleteFileResult +import com.pubnub.api.models.consumer.objects.PNMemberKey +import com.pubnub.api.models.consumer.objects.PNPage +import com.pubnub.api.models.consumer.objects.PNSortKey +import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata +import com.pubnub.api.models.consumer.push.PNPushAddChannelResult +import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult +import com.pubnub.chat.restrictions.GetRestrictionsResponse +import com.pubnub.chat.restrictions.Restriction +import com.pubnub.chat.types.ChannelType +import com.pubnub.chat.types.EventContent +import com.pubnub.chat.types.GetEventsHistoryResult +import com.pubnub.chat.types.GetFilesResult +import com.pubnub.chat.types.HistoryResponse +import com.pubnub.chat.types.InputFile +import com.pubnub.chat.types.MessageMentionedUsers +import com.pubnub.chat.types.MessageReferencedChannels +import com.pubnub.chat.types.TextLink +import com.pubnub.kmp.CustomObject +import com.pubnub.kmp.PNFuture + +/** + * Channel is an object that refers to a single chat room. + */ +interface BaseChannel, M : BaseMessage> { + /** + * Reference to the main Chat object. + */ + val chat: Chat + + /** + * Unique identifier for the channel. + */ + val id: String + + /** + * Display name or title of the channel. + */ + val name: String? + + /** + * Any custom properties or metadata associated with the channel in the form of a map of key-value pairs. + */ + val custom: Map? + + /** + * Brief description or summary of the channel's purpose or content. + */ + val description: String? + + /** + * Timestamp for the last time the channel was updated or modified. + */ + val updated: String? + + /** + * Current status of the channel, like online, offline, or archived. + */ + val status: String? + + /** + * Represents the type of channel, which can be one of the following: + * + * - `ChannelType.DIRECT`: A one-on-one chat between two participants. + * - `ChannelType.GROUP`: A private group chat restricted to invited participants. + * - `ChannelType.PUBLIC`: A public chat open to a large audience, where anyone can join. + * - `ChannelType.UNKNOWN`: Used for channels created with the Kotlin SDK, where the channel type + * in the metadata does not match any of the three default Chat SDK types. + */ + val type: ChannelType? + + /** + * Allows to update the [BaseChannel] metadata + * + * @param name Display name for the channel. + * @param custom Any custom properties or metadata associated with the channel in the form of a `Map`. + * Values must be scalar only; arrays or objects are not supported. + * @param description Additional details about the channel. + * @param status Current status of the channel, like online, offline, or archived. + * @param type Represents the type of channel, which can be one of the following: + * - `ChannelType.DIRECT`: A one-on-one chat between two participants. + * - `ChannelType.GROUP`: A private group chat restricted to invited participants. + * - `ChannelType.PUBLIC`: A public chat open to a large audience, where anyone can join. + * - `ChannelType.UNKNOWN`: Used for channels created with the Kotlin SDK, where the channel type + * in the metadata does not match any of the three default Chat SDK types. + * + * @return [PNFuture] containing the updated [BaseChannel] object with its metadata. + */ + fun update( + name: String? = null, + custom: CustomObject? = null, + description: String? = null, + status: String? = null, + type: ChannelType? = null, + ): PNFuture + + /** + * Allows to delete an existing [BaseChannel] (with or without deleting its historical data from the App Context storage) + * + * @param soft Decide if you want to permanently remove channel metadata. The channel metadata gets permanently + * deleted from the App Context storage by default. If you set this parameter to true, the Channel object + * gets the deleted status, and you can still restore/get its data. + * @return For hard delete, the method returns [PNFuture] without a value (`null`). + * For soft delete, [PNFuture] containing an updated [Channel] instance with the status field set to "deleted". + */ + fun delete(soft: Boolean = false): PNFuture + + /** + * Forwards message to existing [BaseChannel] + * + * @param message Message object that you want to forward to the [BaseChannel]. + * + * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the forwarded message. + */ + fun forwardMessage(message: BaseMessage<*, *>): PNFuture + + /** + * Activates a typing indicator on a given channel. + * The method sets a flag (typingSent) to indicate that a typing signal is in progress and adds a timer to reset + * the flag after a specified timeout. + * + * You can change the default typing timeout and set your own value during the Chat SDK configuration (init() method) + * using the [com.pubnub.chat.config.ChatConfiguration.typingTimeout] parameter. + * + * @return [PNFuture] representing the typing action. The result of this future + * can be handled using the `async` method of `PNFuture`. + * + * Example usage: + * ``` + * channel.startTyping().async { result -> + * result.onSuccess { + * // Handle success + * }.onFailure { error -> + * // Handle error + * } + * } + * ``` + */ + fun startTyping(): PNFuture + + /** + * Deactivates a typing indicator on a given channel. + * + * @return [PNFuture] representing the stop typing action. The result of this future + * can be handled using the `async` method of `PNFuture`. + * + * Example usage: + * ``` + * channel.stopTyping().async { result -> + * result.onSuccess { + * // Handle success + * }.onFailure { error -> + * // Handle error + * } + * } + * ``` + */ + fun stopTyping(): PNFuture + + /** + * Enables continuous tracking of typing activity within the [BaseChannel]. + * + * @param callback Callback function passed as a parameter. It defines the custom behavior to be executed whenever + * a user starts/stops typing. + * + * @return AutoCloseable Interface you can call to disconnect (unsubscribe) from the channel and stop receiving + * signal events for someone typing by invoking the close() method. + */ + fun getTyping(callback: (typingUserIds: Collection) -> Unit): AutoCloseable + + /** + * Returns a list of users present on the [BaseChannel] + * + * @return [PNFuture] A future containing a collection of strings representing userId. + * The result of this future can be processed using the `async` method of `PNFuture`. + * + */ + fun whoIsPresent(): PNFuture> + + /** + * Returns information if the user is present on the [BaseChannel] + * + * @param userId ID of the user whose presence you want to check. + * + * @return [PNFuture] with Boolean value informing if a given user is present on a specified [BaseChannel] + */ + fun isPresent(userId: String): PNFuture + + /** + * Returns historical messages for the [BaseChannel] + * + * @param startTimetoken Timetoken delimiting the start of a time slice (exclusive) to pull messages from. + * @param endTimetoken Timetoken delimiting the end of a time slice (inclusive) to pull messages from + * @param count The maximum number of messages to retrieve. Default and maximum values is 25. + * + * @return [PNFuture] containing a list of messages with pagination information (isMore: Boolean). The result of + * this future can be processed using the `async` method of `PNFuture`. + */ + fun getHistory( + startTimetoken: Long? = null, + endTimetoken: Long? = null, + count: Int = 25, + ): PNFuture> + + /** + * Sends text to the [BaseChannel] + * + * @param text Text that you want to send to the selected channel. + * @param meta Publish additional details with the request. + * @param shouldStore If true, the messages are stored in Message Persistence if enabled in Admin Portal. + * @param usePost Use HTTP POST + * @param ttl Defines if / how long (in hours) the message should be stored in Message Persistence. + * If shouldStore = true, and ttl = 0, the message is stored with no expiry time. + * If shouldStore = true and ttl = X, the message is stored with an expiry time of X hours. + * If shouldStore = false, the ttl parameter is ignored. + * If ttl is not specified, then the expiration of the message defaults back to the expiry value for the keyset. + * @param quotedMessage Object added to a message when you quote another message. This object stores the following + * info about the quoted message: timetoken for the time when the quoted message was published, text with the + * original message content, and userId as the identifier of the user who published the quoted message. + * @param files One or multiple files attached to the text message. + * @param usersToMention A collection of user ids to automatically notify with a mention after this message is sent. + * @param customPushData Additional key-value pairs that will be added to the FCM and/or APNS push messages for the + * message itself and any user mentions. + * + * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the sent message. + */ + fun sendText( + text: String, + meta: Map? = null, + shouldStore: Boolean = true, + usePost: Boolean = false, + ttl: Int? = null, + quotedMessage: BaseMessage<*, *>? = null, + files: List? = null, + usersToMention: Collection? = null, + customPushData: Map? = null + ): PNFuture + + /** + * Sends text to the [BaseChannel] + * + * @param text Text that you want to send to the selected channel. + * @param meta Publish additional details with the request. + * @param shouldStore If true, the messages are stored in Message Persistence if enabled in Admin Portal. + * @param usePost Use HTTP POST + * @param ttl Defines if / how long (in hours) the message should be stored in Message Persistence. + * If shouldStore = true, and ttl = 0, the message is stored with no expiry time. + * If shouldStore = true and ttl = X, the message is stored with an expiry time of X hours. + * If shouldStore = false, the ttl parameter is ignored. + * If ttl is not specified, then the expiration of the message defaults to the expiry value for the keyset. + * @param mentionedUsers Object mapping a mentioned user (with name and ID) with the number of mention (like @Mar) + * in the message (relative to other user mentions). + * For example, { 0: { id: 123, name: "Mark" }, 2: { id: 345, name: "Rob" } } means that Mark will be shown on + * the first mention (@) in the message and Rob on the third. + * @param referencedChannels Object mapping the referenced channel (with name and ID) with the place (Int) where + * this reference (like #Sup) was mentioned in the message (relative to other channel references). + * For example, { 0: { id: 123, name: "Support" }, 2: { id: 345, name: "Off-topic" } } means that Support will be + * shown on the first reference in the message and Off-topic on the third. + * @param textLinks Returned list of text links that are shown as text in the message Each TextLink contains these + * fields: class TextLink(val startIndex: Int, val endIndex: Int, val link: String) where startIndex indicates + * the position in the whole message where the link should start and endIndex where it ends. Note that indexing starts with 0. + * @param quotedMessage Object added to a message when you quote another message. This object stores the following + * info about the quoted message: timetoken for the time when the quoted message was published, text with the + * original message content, and userId as the identifier of the user who published the quoted message. + * @param files One or multiple files attached to the text message. + * @param customPushData Additional key-value pairs that will be added to the FCM and/or APNS push messages for the + * message itself and any user mentions. + * + * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the sent message. + */ + @Deprecated( + message = "Will be removed from SDK in the future", + level = DeprecationLevel.WARNING, + ) + fun sendText( + text: String, + meta: Map? = null, + shouldStore: Boolean = true, + usePost: Boolean = false, + ttl: Int? = null, + mentionedUsers: MessageMentionedUsers? = null, + referencedChannels: MessageReferencedChannels? = null, + textLinks: List? = null, + quotedMessage: BaseMessage<*, *>? = null, + files: List? = null, + customPushData: Map? = null + ): PNFuture + + /** + * Watch the [BaseChannel] content without a need to [join] the [BaseChannel] + * + * @param callback defines the custom behavior to be executed whenever a message is received on the [BaseChannel] + * + * @return AutoCloseable Interface you can call to stop listening for new messages and clean up resources when they + * are no longer needed by invoking the close() method. + */ + fun connect(callback: (M) -> Unit): AutoCloseable + + /** + * Fetches the message that is currently pinned to the channel. There can be only one pinned message on a channel at a time. + * + * @return [PNFuture] containing pinned [Message] + */ + fun getPinnedMessage(): PNFuture?> + + /** + * Attaches messages to the [BaseChannel]. Replace an already pinned message. There can be only one pinned message on a channel at a time. + * + * @param message that you want to pin to the selected channel. + * + * @return [PNFuture] containing [BaseChannel] with updated [BaseChannel.custom] + */ + fun pinMessage(message: BaseMessage<*, *>): PNFuture + + /** + * Unpins a message from the [BaseChannel]. + * + * @return [PNFuture] containing [BaseChannel] with updated [BaseChannel.custom] + */ + fun unpinMessage(): PNFuture + + /** + * Fetches the [Message] from Message Persistence based on the message [Message.timetoken]. + * + * @param timetoken of the message you want to retrieve from Message Persistence + * + * @return [PNFuture] containing [Message] + */ + fun getMessage(timetoken: Long): PNFuture + + /** + * Register a device on the [BaseChannel] to receive push notifications. Push options can be configured in [com.pubnub.chat.config.ChatConfiguration] + * + * @return [PNFuture] containing [PNPushAddChannelResult] + */ + fun registerForPush(): PNFuture + + /** + * Unregister a device from the [BaseChannel] + * + * @return [PNFuture] containing [PNPushRemoveChannelResult] + */ + fun unregisterFromPush(): PNFuture + + /** + * Allows to mute/ban a specific user on a channel or unmute/unban them. + * + * Please note that this is a server-side moderation mechanism, as opposed to [Chat.mutedUsersManager] (which is local to + * a client). + * + * @param user to be muted or banned. + * @param ban represents the user's moderation restrictions. Set to true to ban the user from the channel or to false to unban them. + * @param mute represents the user's moderation restrictions. Set to true to mute the user on the channel or to false to unmute them. + * @param reason Reason why you want to ban/mute the user. + * + * @return [PNFuture] that will be completed with Unit. + */ + fun setRestrictions( + user: User, + ban: Boolean = false, + mute: Boolean = false, + reason: String? = null + ): PNFuture + + /** + * Check user's restrictions. + * + * @param user to be checked permission for. + * + * @return [PNFuture] containing [Restriction] + */ + fun getUserRestrictions(user: User): PNFuture + + /** + * Check if there are any mute or ban restrictions set for users on a given channel + * + * @param limit Number of objects to return in response. The default (and maximum) value is 100. + * @param page Object used for pagination to define which previous or next result page you want to fetch. + * @param sort A collection to specify the sort order. Available options are id, name, and updated. Use asc or desc + * to specify the sorting direction, or specify null to take the default sorting direction (ascending). + * + * @return [PNFuture] containing [GetRestrictionsResponse] + */ + fun getUsersRestrictions( + limit: Int? = null, + page: PNPage? = null, + sort: Collection> = listOf() + ): PNFuture + + /** + * Receives updates on a single Channel object. + * + * @param callback Function that takes a single Channel object. It defines the custom behavior to be executed when detecting channel changes. + * + * @return [AutoCloseable] interface that lets you stop receiving channel-related updates (objects events) + * and clean up resources by invoking the close() method. + */ + fun streamUpdates(callback: (channel: C?) -> Unit): AutoCloseable + + /** + * Returns all files attached to messages on a given channel. + * + * @param limit Number of files to return(default and max is 100) + * @param next Token to get the next batch of files. + * + * @return [PNFuture] containing [GetFilesResult] + */ + fun getFiles(limit: Int = 100, next: String? = null): PNFuture + + /** + * Delete sent files or files from published messages. + * + * @param id Unique identifier assigned to the file by PubNub. + * @param name Name of the file. + * + * @return [PNFuture] containing [PNDeleteFileResult] + */ + fun deleteFile(id: String, name: String): PNFuture + + /** + * Enables real-time tracking of users connecting to or disconnecting from a [BaseChannel]. + + * @param callback defines the custom behavior to be executed when detecting user presence event. + * + * @return AutoCloseable Interface that lets you stop receiving presence-related updates (presence events) by invoking the close() method. + */ + fun streamPresence(callback: (userIds: Collection) -> Unit): AutoCloseable + + /** + * Fetches a list of reported message events for [BaseChannel] within optional time and count constraints. + * + * @param startTimetoken The start timetoken for fetching the history of reported messages, which allows specifying + * the point in time where the history retrieval should begin. + * @param endTimetoken The end time token for fetching the history of reported messages, which allows specifying + * the point in time where the history retrieval should end. + * @param count The number of reported message events to fetch from the history. Default and max is 100. + * + * @return [PNFuture] containing set of [GetEventsHistoryResult] + */ + fun getMessageReportsHistory( + startTimetoken: Long? = null, + endTimetoken: Long? = null, + count: Int = 100, + ): PNFuture + + /** + * As an admin of your chat app, monitor all events emitted when someone reports an offensive message. + * + * @param callback Callback function passed as a parameter. It defines the custom behavior to be executed when + * detecting new message report events. + * + * @return AutoCloseable Interface that lets you stop receiving report-related updates (report events) by invoking the close() method. + */ + fun streamMessageReports(callback: (event: Event) -> Unit): AutoCloseable + + /** + * Fetches all suggested users that match the provided 3-letter string from [BaseChannel] + * + * @param text at least a 3-letter string typed in after @ with the user name you want to mention. + * @param limit maximum number of returned usernames that match the typed 3-letter suggestion. The default value is set to 10, and the maximum is 100. + * + * @return [PNFuture] containing list of [Membership] + */ + fun getUserSuggestions(text: String, limit: Int = 10): PNFuture> + + /** + * Get a new `Channel` instance that is a copy of this `Channel` with its properties updated with information coming from `update`. + */ + operator fun plus(update: PNChannelMetadata): C + + // Companion object required for extending this class elsewhere + companion object +} diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/BaseMessage.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/BaseMessage.kt new file mode 100644 index 00000000..05c04c71 --- /dev/null +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/BaseMessage.kt @@ -0,0 +1,192 @@ +package com.pubnub.chat + +import com.pubnub.api.PubNubError +import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.api.models.consumer.history.PNFetchMessageItem.Action +import com.pubnub.chat.types.EventContent +import com.pubnub.chat.types.File +import com.pubnub.chat.types.MessageMentionedUsers +import com.pubnub.chat.types.MessageReferencedChannels +import com.pubnub.chat.types.QuotedMessage +import com.pubnub.chat.types.TextLink +import com.pubnub.kmp.PNFuture + +/** + * Represents an object that refers to a single message in a chat. + */ +interface BaseMessage, C : BaseChannel> { + /** + * Reference to the main Chat object. + */ + val chat: Chat + + /** + * Unique identifier for the message. + */ + val timetoken: Long + + /** + * Original text content of the message. + */ + val content: EventContent.TextMessageContent + + /** + * Unique identifier for the channel in which the message was sent. + */ + val channelId: String + + /** + * Unique ID of the user who sent the message. + */ + val userId: String + + /** + * Any actions associated with the message, such as reactions, replies, or other interactive elements. + */ + val actions: Map>>? + + /** + * Extra information added to the message giving additional context. + */ + val meta: Map? + + /** + * Access the original quoted message. + * + * `quotedMessage` returns only values for the timetoken, text, and userId parameters. If you want to return the + * full quoted Message object, use the [Channel.getMessage] method and the timetoken from the quote that you can + * extract from the `quotedMessage` parameter added to the published message. + */ + val quotedMessage: QuotedMessage? + + /** + * Content of the message. + */ + val text: String + + /** + * Whether the message is soft deleted. + */ + val deleted: Boolean + + /** + * Message type (currently "text" for all Messages). + */ + val type: String + + /** + * List of attached files with their names, types, and sources. + */ + val files: List + + /** + * List of reactions attached to the message. + */ + val reactions: Map> + + /** + * Error associated with the message, if any. + */ + val error: PubNubError? + + /** + * List of included text links and their position. + */ + @Deprecated("Use `Message.getMessageElements()` instead.") + val textLinks: List? + + /** + * List of mentioned users with IDs and names. + */ + @Deprecated("Use `Message.getMessageElements()` instead.") + val mentionedUsers: MessageMentionedUsers? + + /** + * List of referenced channels with IDs and names. + */ + @Deprecated("Use `Message.getMessageElements()` instead.") + val referencedChannels: MessageReferencedChannels? + + /** + * Checks if the current user added a given emoji to the message. + * + * @param reaction Specific emoji added to the message. + * @return Specifies if the current user added a given emoji to the message or not. + */ + fun hasUserReaction(reaction: String): Boolean + + /** + * Changes the content of the existing message to a new one. + * + * @param newText New/updated text that you want to add in place of the existing message. + * @return An updated message instance with an added `edited` action type. + */ + fun editText(newText: String): PNFuture + + /** + * Either permanently removes a historical message from Message Persistence or marks it as deleted (if you remove the message with the soft option). + * + * Requires Message Persistence configuration. To manage messages, you must enable Message Persistence for your app's keyset in the Admin Portal. To delete messages from PubNub storage, you must also mark the Enable Delete-From-History option. + * + * @param soft Decide if you want to permanently remove message data. By default, the message data gets permanently deleted from Message Persistence. If you set this parameter to true, the Message object gets the deleted status and you can still restore/get its data. + * @param preserveFiles Define if you want to keep the files attached to the message or remove them. + * @return For hard delete, the method returns `PNFuture` without a value (`null`). For soft delete, a `PNFuture` with an updated message instance with an added deleted action type. + */ + fun delete(soft: Boolean = false, preserveFiles: Boolean = false): PNFuture + + /** + * Forward a given message from one channel to another. + * + * @param channelId Unique identifier of the channel to which you want to forward the message. You can forward a message to the same channel on which it was published or to any other. + * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the forwarded message. + */ + fun forward(channelId: String): PNFuture + + /** + * Attach this message to its channel. + * + * @return `PNFuture` containing the updated channel metadata + */ + fun pin(): PNFuture + + /** + * Flag and report an inappropriate message to the admin. + * + * @param reason Reason for reporting/flagging a given message. + * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the report message. + */ + fun report(reason: String): PNFuture + + /** + * Add or remove a reaction to a message. + * + * `toggleReaction()` is a method for both adding and removing message reactions. It adds a string flag to the message if the current user hasn't added it yet or removes it if the current user already added it before. + * + * If you use this method to add or remove message reactions, this flag would be a literal emoji you could implement in your app's UI. However, you could also use this method for a different purpose, like marking a message as pinned to a channel or unpinned if you implement the pinning feature in your chat app. + * + * @param reaction Emoji added to the message or removed from it by the current user. + * @return Updated message instance with an added reactions action type. + */ + fun toggleReaction(reaction: String): PNFuture + + /** + * You can receive updates when this message and related message reactions are added, edited, or removed. + * + * @param callback Function that takes a single Message object. It defines the custom behavior to be executed when detecting message or message reaction changes. + * @return Interface that lets you stop receiving message-related updates by invoking the close() method + */ + fun streamUpdates(callback: (message: M) -> Unit): AutoCloseable + + /** + * If you delete a message, you can restore its content together with the attached files using the restore() method. + * + * This is possible, however, only if the message you want to restore was soft deleted (the soft parameter was set to true when deleting it). Hard deleted messages cannot be restored as their data is no longer available in Message Persistence. + * + * Requires Message Persistence configuration. To manage messages, you must enable Message Persistence for your app's keyset in the Admin Portal and mark the Enable Delete-From-History option. + * + * @return Object returning the restored Message object. + */ + fun restore(): PNFuture + + companion object +} diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Channel.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Channel.kt index 5dbe43aa..e5be066d 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Channel.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Channel.kt @@ -1,293 +1,41 @@ package com.pubnub.chat -import com.pubnub.api.models.consumer.PNPublishResult -import com.pubnub.api.models.consumer.files.PNDeleteFileResult import com.pubnub.api.models.consumer.objects.PNMemberKey import com.pubnub.api.models.consumer.objects.PNPage import com.pubnub.api.models.consumer.objects.PNSortKey import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata -import com.pubnub.api.models.consumer.push.PNPushAddChannelResult -import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult import com.pubnub.chat.membership.MembersResponse -import com.pubnub.chat.restrictions.GetRestrictionsResponse -import com.pubnub.chat.restrictions.Restriction import com.pubnub.chat.types.ChannelType -import com.pubnub.chat.types.EventContent -import com.pubnub.chat.types.GetEventsHistoryResult -import com.pubnub.chat.types.GetFilesResult import com.pubnub.chat.types.HistoryResponse -import com.pubnub.chat.types.InputFile import com.pubnub.chat.types.JoinResult -import com.pubnub.chat.types.MessageMentionedUsers -import com.pubnub.chat.types.MessageReferencedChannels -import com.pubnub.chat.types.TextLink import com.pubnub.kmp.CustomObject import com.pubnub.kmp.PNFuture /** * Channel is an object that refers to a single chat room. */ -interface Channel { - /** - * Reference to the main Chat object. - */ - val chat: Chat - - /** - * Unique identifier for the channel. - */ - val id: String - - /** - * Display name or title of the channel. - */ - val name: String? - - /** - * Any custom properties or metadata associated with the channel in the form of a map of key-value pairs. - */ - val custom: Map? - - /** - * Brief description or summary of the channel's purpose or content. - */ - val description: String? - - /** - * Timestamp for the last time the channel was updated or modified. - */ - val updated: String? - - /** - * Current status of the channel, like online, offline, or archived. - */ - val status: String? - - /** - * Represents the type of channel, which can be one of the following: - * - * - `ChannelType.DIRECT`: A one-on-one chat between two participants. - * - `ChannelType.GROUP`: A private group chat restricted to invited participants. - * - `ChannelType.PUBLIC`: A public chat open to a large audience, where anyone can join. - * - `ChannelType.UNKNOWN`: Used for channels created with the Kotlin SDK, where the channel type - * in the metadata does not match any of the three default Chat SDK types. - */ - val type: ChannelType? - - /** - * Allows to update the [Channel] metadata - * - * @param name Display name for the channel. - * @param custom Any custom properties or metadata associated with the channel in the form of a `Map`. - * Values must be scalar only; arrays or objects are not supported. - * @param description Additional details about the channel. - * @param status Current status of the channel, like online, offline, or archived. - * @param type Represents the type of channel, which can be one of the following: - * - `ChannelType.DIRECT`: A one-on-one chat between two participants. - * - `ChannelType.GROUP`: A private group chat restricted to invited participants. - * - `ChannelType.PUBLIC`: A public chat open to a large audience, where anyone can join. - * - `ChannelType.UNKNOWN`: Used for channels created with the Kotlin SDK, where the channel type - * in the metadata does not match any of the three default Chat SDK types. - * - * @return [PNFuture] containing the updated [Channel] object with its metadata. - */ - fun update( - name: String? = null, - custom: CustomObject? = null, - description: String? = null, - status: String? = null, - type: ChannelType? = null, +interface Channel : BaseChannel { + override fun update( + name: String?, + custom: CustomObject?, + description: String?, + status: String?, + type: ChannelType?, ): PNFuture - /** - * Allows to delete an existing [Channel] (with or without deleting its historical data from the App Context storage) - * - * @param soft Decide if you want to permanently remove channel metadata. The channel metadata gets permanently - * deleted from the App Context storage by default. If you set this parameter to true, the Channel object - * gets the deleted status, and you can still restore/get its data. - * @return For hard delete, the method returns [PNFuture] without a value (`null`). - * For soft delete, [PNFuture] containing an updated [Channel] instance with the status field set to "deleted". - */ - fun delete(soft: Boolean = false): PNFuture - - /** - * Forwards message to existing [Channel] - * - * @param message Message object that you want to forward to the [Channel]. - * - * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the forwarded message. - */ - fun forwardMessage(message: Message): PNFuture - - /** - * Activates a typing indicator on a given channel. - * The method sets a flag (typingSent) to indicate that a typing signal is in progress and adds a timer to reset - * the flag after a specified timeout. - * - * You can change the default typing timeout and set your own value during the Chat SDK configuration (init() method) - * using the [com.pubnub.chat.config.ChatConfiguration.typingTimeout] parameter. - * - * @return [PNFuture] representing the typing action. The result of this future - * can be handled using the `async` method of `PNFuture`. - * - * Example usage: - * ``` - * channel.startTyping().async { result -> - * result.onSuccess { - * // Handle success - * }.onFailure { error -> - * // Handle error - * } - * } - * ``` - */ - fun startTyping(): PNFuture - - /** - * Deactivates a typing indicator on a given channel. - * - * @return [PNFuture] representing the stop typing action. The result of this future - * can be handled using the `async` method of `PNFuture`. - * - * Example usage: - * ``` - * channel.stopTyping().async { result -> - * result.onSuccess { - * // Handle success - * }.onFailure { error -> - * // Handle error - * } - * } - * ``` - */ - fun stopTyping(): PNFuture + override fun delete(soft: Boolean): PNFuture - /** - * Enables continuous tracking of typing activity within the [Channel]. - * - * @param callback Callback function passed as a parameter. It defines the custom behavior to be executed whenever - * a user starts/stops typing. - * - * @return AutoCloseable Interface you can call to disconnect (unsubscribe) from the channel and stop receiving - * signal events for someone typing by invoking the close() method. - */ - fun getTyping(callback: (typingUserIds: Collection) -> Unit): AutoCloseable + override fun connect(callback: (Message) -> Unit): AutoCloseable - /** - * Returns a list of users present on the [Channel] - * - * @return [PNFuture] A future containing a collection of strings representing userId. - * The result of this future can be processed using the `async` method of `PNFuture`. - * - */ - fun whoIsPresent(): PNFuture> + override fun unpinMessage(): PNFuture - /** - * Returns information if the user is present on the [Channel] - * - * @param userId ID of the user whose presence you want to check. - * - * @return [PNFuture] with Boolean value informing if a given user is present on a specified [Channel] - */ - fun isPresent(userId: String): PNFuture + override fun getMessage(timetoken: Long): PNFuture - /** - * Returns historical messages for the [Channel] - * - * @param startTimetoken Timetoken delimiting the start of a time slice (exclusive) to pull messages from. - * @param endTimetoken Timetoken delimiting the end of a time slice (inclusive) to pull messages from - * @param count The maximum number of messages to retrieve. Default and maximum values is 25. - * - * @return [PNFuture] containing a list of messages with pagination information (isMore: Boolean). The result of - * this future can be processed using the `async` method of `PNFuture`. - */ - fun getHistory( - startTimetoken: Long? = null, - endTimetoken: Long? = null, - count: Int = 25, - ): PNFuture> + override fun streamUpdates(callback: (channel: Channel?) -> Unit): AutoCloseable - /** - * Sends text to the [Channel] - * - * @param text Text that you want to send to the selected channel. - * @param meta Publish additional details with the request. - * @param shouldStore If true, the messages are stored in Message Persistence if enabled in Admin Portal. - * @param usePost Use HTTP POST - * @param ttl Defines if / how long (in hours) the message should be stored in Message Persistence. - * If shouldStore = true, and ttl = 0, the message is stored with no expiry time. - * If shouldStore = true and ttl = X, the message is stored with an expiry time of X hours. - * If shouldStore = false, the ttl parameter is ignored. - * If ttl is not specified, then the expiration of the message defaults back to the expiry value for the keyset. - * @param quotedMessage Object added to a message when you quote another message. This object stores the following - * info about the quoted message: timetoken for the time when the quoted message was published, text with the - * original message content, and userId as the identifier of the user who published the quoted message. - * @param files One or multiple files attached to the text message. - * @param usersToMention A collection of user ids to automatically notify with a mention after this message is sent. - * @param customPushData Additional key-value pairs that will be added to the FCM and/or APNS push messages for the - * message itself and any user mentions. - * - * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the sent message. - */ - fun sendText( - text: String, - meta: Map? = null, - shouldStore: Boolean = true, - usePost: Boolean = false, - ttl: Int? = null, - quotedMessage: Message? = null, - files: List? = null, - usersToMention: Collection? = null, - customPushData: Map? = null - ): PNFuture + override fun getHistory(startTimetoken: Long?, endTimetoken: Long?, count: Int): PNFuture> - /** - * Sends text to the [Channel] - * - * @param text Text that you want to send to the selected channel. - * @param meta Publish additional details with the request. - * @param shouldStore If true, the messages are stored in Message Persistence if enabled in Admin Portal. - * @param usePost Use HTTP POST - * @param ttl Defines if / how long (in hours) the message should be stored in Message Persistence. - * If shouldStore = true, and ttl = 0, the message is stored with no expiry time. - * If shouldStore = true and ttl = X, the message is stored with an expiry time of X hours. - * If shouldStore = false, the ttl parameter is ignored. - * If ttl is not specified, then the expiration of the message defaults to the expiry value for the keyset. - * @param mentionedUsers Object mapping a mentioned user (with name and ID) with the number of mention (like @Mar) - * in the message (relative to other user mentions). - * For example, { 0: { id: 123, name: "Mark" }, 2: { id: 345, name: "Rob" } } means that Mark will be shown on - * the first mention (@) in the message and Rob on the third. - * @param referencedChannels Object mapping the referenced channel (with name and ID) with the place (Int) where - * this reference (like #Sup) was mentioned in the message (relative to other channel references). - * For example, { 0: { id: 123, name: "Support" }, 2: { id: 345, name: "Off-topic" } } means that Support will be - * shown on the first reference in the message and Off-topic on the third. - * @param textLinks Returned list of text links that are shown as text in the message Each TextLink contains these - * fields: class TextLink(val startIndex: Int, val endIndex: Int, val link: String) where startIndex indicates - * the position in the whole message where the link should start and endIndex where it ends. Note that indexing starts with 0. - * @param quotedMessage Object added to a message when you quote another message. This object stores the following - * info about the quoted message: timetoken for the time when the quoted message was published, text with the - * original message content, and userId as the identifier of the user who published the quoted message. - * @param files One or multiple files attached to the text message. - * - * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the sent message. - */ - @Deprecated( - message = "Will be removed from SDK in the future", - level = DeprecationLevel.WARNING, - ) - fun sendText( - text: String, - meta: Map? = null, - shouldStore: Boolean = true, - usePost: Boolean = false, - ttl: Int? = null, - mentionedUsers: MessageMentionedUsers? = null, - referencedChannels: MessageReferencedChannels? = null, - textLinks: List? = null, - quotedMessage: Message? = null, - files: List? = null, - customPushData: Map? = null, - ): PNFuture + override operator fun plus(update: PNChannelMetadata): Channel /** * Requests another user to join a channel(except Public channel) and become its member. @@ -301,7 +49,7 @@ interface Channel { /** * Requests other users to join a channel and become its members. You can invite up to 100 users at once. * - * @param users List of users you want to invite to the [Channel]. You can invite up to 100 users in one call. + * @param users List of users you want to invite to the [BaseChannel]. You can invite up to 100 users in one call. * * @return [PNFuture] containing list of [Membership] of invited users. */ @@ -326,23 +74,13 @@ interface Channel { ): PNFuture /** - * Watch the [Channel] content without a need to [join] the [Channel] - * - * @param callback defines the custom behavior to be executed whenever a message is received on the [Channel] - * - * @return AutoCloseable Interface you can call to stop listening for new messages and clean up resources when they - * are no longer needed by invoking the close() method. - */ - fun connect(callback: (Message) -> Unit): AutoCloseable - - /** - * Connects a user to the [Channel] and sets membership - this way, the chat user can both watch the channel's + * Connects a user to the [BaseChannel] and sets membership - this way, the chat user can both watch the channel's * content and be its full-fledged member. * * @param custom Any custom properties or metadata associated with the channel-user membership in the form of a `Map`. * Values must be scalar only; arrays or objects are not supported. * a JSON. Values must be scalar only; arrays or objects are not supported. - * @param callback defines the custom behavior to be executed whenever a message is received on the [Channel] + * @param callback defines the custom behavior to be executed whenever a message is received on the [BaseChannel] * * @return [PNFuture] containing [JoinResult] that contains the [JoinResult.membership] and * [JoinResult.disconnect] that lets you stop listening to new channel messages or message updates while remaining @@ -352,111 +90,10 @@ interface Channel { fun join(custom: CustomObject? = null, callback: ((Message) -> Unit)? = null): PNFuture /** - * Remove user's [Channel] membership + * Remove user's [BaseChannel] membership */ fun leave(): PNFuture - /** - * Fetches the message that is currently pinned to the channel. There can be only one pinned message on a channel at a time. - * - * @return [PNFuture] containing pinned [Message] - */ - fun getPinnedMessage(): PNFuture - - /** - * Attaches messages to the [Channel]. Replace an already pinned message. There can be only one pinned message on a channel at a time. - * - * @param message that you want to pin to the selected channel. - * - * @return [PNFuture] containing [Channel] with updated [Channel.custom] - */ - fun pinMessage(message: Message): PNFuture - - /** - * Unpins a message from the [Channel]. - * - * @return [PNFuture] containing [Channel] with updated [Channel.custom] - */ - fun unpinMessage(): PNFuture - - /** - * Fetches the [Message] from Message Persistence based on the message [Message.timetoken]. - * - * @param timetoken of the message you want to retrieve from Message Persistence - * - * @return [PNFuture] containing [Message] - */ - fun getMessage(timetoken: Long): PNFuture - - /** - * Register a device on the [Channel] to receive push notifications. Push options can be configured in [com.pubnub.chat.config.ChatConfiguration] - * - * @return [PNFuture] containing [PNPushAddChannelResult] - */ - fun registerForPush(): PNFuture - - /** - * Unregister a device from the [Channel] - * - * @return [PNFuture] containing [PNPushRemoveChannelResult] - */ - fun unregisterFromPush(): PNFuture - - /** - * Allows to mute/ban a specific user on a channel or unmute/unban them. - * - * Please note that this is a server-side moderation mechanism, as opposed to [Chat.mutedUsersManager] (which is local to - * a client). - * - * @param user to be muted or banned. - * @param ban represents the user's moderation restrictions. Set to true to ban the user from the channel or to false to unban them. - * @param mute represents the user's moderation restrictions. Set to true to mute the user on the channel or to false to unmute them. - * @param reason Reason why you want to ban/mute the user. - * - * @return [PNFuture] that will be completed with Unit. - */ - fun setRestrictions( - user: User, - ban: Boolean = false, - mute: Boolean = false, - reason: String? = null - ): PNFuture - - /** - * Check user's restrictions. - * - * @param user to be checked permission for. - * - * @return [PNFuture] containing [Restriction] - */ - fun getUserRestrictions(user: User): PNFuture - - /** - * Check if there are any mute or ban restrictions set for users on a given channel - * - * @param limit Number of objects to return in response. The default (and maximum) value is 100. - * @param page Object used for pagination to define which previous or next result page you want to fetch. - * @param sort A collection to specify the sort order. Available options are id, name, and updated. Use asc or desc - * to specify the sorting direction, or specify null to take the default sorting direction (ascending). - * - * @return [PNFuture] containing [GetRestrictionsResponse] - */ - fun getUsersRestrictions( - limit: Int? = null, - page: PNPage? = null, - sort: Collection> = listOf() - ): PNFuture - - /** - * Receives updates on a single Channel object. - * - * @param callback Function that takes a single Channel object. It defines the custom behavior to be executed when detecting channel changes. - * - * @return [AutoCloseable] interface that lets you stop receiving channel-related updates (objects events) - * and clean up resources by invoking the close() method. - */ - fun streamUpdates(callback: (channel: Channel?) -> Unit): AutoCloseable - /** * Lets you get a read confirmation status for messages you published on a channel. * @param callback defines the custom behavior to be executed when receiving a read confirmation status on the joined channel. @@ -466,77 +103,6 @@ interface Channel { */ fun streamReadReceipts(callback: (receipts: Map>) -> Unit): AutoCloseable - /** - * Returns all files attached to messages on a given channel. - * - * @param limit Number of files to return(default and max is 100) - * @param next Token to get the next batch of files. - * - * @return [PNFuture] containing [GetFilesResult] - */ - fun getFiles(limit: Int = 100, next: String? = null): PNFuture - - /** - * Delete sent files or files from published messages. - * - * @param id Unique identifier assigned to the file by PubNub. - * @param name Name of the file. - * - * @return [PNFuture] containing [PNDeleteFileResult] - */ - fun deleteFile(id: String, name: String): PNFuture - - /** - * Enables real-time tracking of users connecting to or disconnecting from a [Channel]. - - * @param callback defines the custom behavior to be executed when detecting user presence event. - * - * @return AutoCloseable Interface that lets you stop receiving presence-related updates (presence events) by invoking the close() method. - */ - fun streamPresence(callback: (userIds: Collection) -> Unit): AutoCloseable - - /** - * Fetches all suggested users that match the provided 3-letter string from [Channel] - * - * @param text at least a 3-letter string typed in after @ with the user name you want to mention. - * @param limit maximum number of returned usernames that match the typed 3-letter suggestion. The default value is set to 10, and the maximum is 100. - * - * @return [PNFuture] containing list of [Membership] - */ - fun getUserSuggestions(text: String, limit: Int = 10): PNFuture> - - /** - * Fetches a list of reported message events for [Channel] within optional time and count constraints. - * - * @param startTimetoken The start timetoken for fetching the history of reported messages, which allows specifying - * the point in time where the history retrieval should begin. - * @param endTimetoken The end time token for fetching the history of reported messages, which allows specifying - * the point in time where the history retrieval should end. - * @param count The number of reported message events to fetch from the history. Default and max is 100. - * - * @return [PNFuture] containing set of [GetEventsHistoryResult] - */ - fun getMessageReportsHistory( - startTimetoken: Long? = null, - endTimetoken: Long? = null, - count: Int = 100, - ): PNFuture - - /** - * As an admin of your chat app, monitor all events emitted when someone reports an offensive message. - * - * @param callback Callback function passed as a parameter. It defines the custom behavior to be executed when - * detecting new message report events. - * - * @return AutoCloseable Interface that lets you stop receiving report-related updates (report events) by invoking the close() method. - */ - fun streamMessageReports(callback: (event: Event) -> Unit): AutoCloseable - - /** - * Get a new `Channel` instance that is a copy of this `Channel` with its properties updated with information coming from `update`. - */ - operator fun plus(update: PNChannelMetadata): Channel - // Companion object required for extending this class elsewhere companion object } diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Membership.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Membership.kt index 9262a99e..105b23d0 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Membership.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Membership.kt @@ -60,7 +60,7 @@ interface Membership { * @param message Last read message on a given channel with the timestamp that gets added to the user-channel membership as the [lastReadMessageTimetoken] property. * @return A [PNFuture] that returns an updated [Membership] object. */ - fun setLastReadMessage(message: Message): PNFuture + fun setLastReadMessage(message: BaseMessage<*, *>): PNFuture /** * Updates the channel membership information for a given user. diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Message.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Message.kt index 36893ccb..623e46be 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Message.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Message.kt @@ -1,175 +1,17 @@ package com.pubnub.chat -import com.pubnub.api.PubNubError -import com.pubnub.api.models.consumer.PNPublishResult -import com.pubnub.api.models.consumer.history.PNFetchMessageItem.Action import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult -import com.pubnub.chat.types.EventContent -import com.pubnub.chat.types.File -import com.pubnub.chat.types.MessageMentionedUsers -import com.pubnub.chat.types.MessageReferencedChannels -import com.pubnub.chat.types.QuotedMessage -import com.pubnub.chat.types.TextLink import com.pubnub.kmp.PNFuture /** * Represents an object that refers to a single message in a chat. */ -interface Message { - /** - * Reference to the main Chat object. - */ - val chat: Chat - - /** - * Unique identifier for the message. - */ - val timetoken: Long - - /** - * Original text content of the message. - */ - val content: EventContent.TextMessageContent - - /** - * Unique identifier for the channel in which the message was sent. - */ - val channelId: String - - /** - * Unique ID of the user who sent the message. - */ - val userId: String - - /** - * Any actions associated with the message, such as reactions, replies, or other interactive elements. - */ - val actions: Map>>? - - /** - * Extra information added to the message giving additional context. - */ - val meta: Map? - - /** - * Access the original quoted message. - * - * `quotedMessage` returns only values for the timetoken, text, and userId parameters. If you want to return the - * full quoted Message object, use the [Channel.getMessage] method and the timetoken from the quote that you can - * extract from the `quotedMessage` parameter added to the published message. - */ - val quotedMessage: QuotedMessage? - - /** - * Content of the message. - */ - val text: String - - /** - * Whether the message is soft deleted. - */ - val deleted: Boolean - +interface Message : BaseMessage { /** * Whether any thread has been created for this message. */ val hasThread: Boolean - /** - * Message type (currently "text" for all Messages). - */ - val type: String - - /** - * List of attached files with their names, types, and sources. - */ - val files: List - - /** - * List of reactions attached to the message. - */ - val reactions: Map> - - /** - * Error associated with the message, if any. - */ - val error: PubNubError? - - /** - * List of included text links and their position. - */ - @Deprecated("Use `Message.getMessageElements()` instead.") - val textLinks: List? - - /** - * List of mentioned users with IDs and names. - */ - @Deprecated("Use `Message.getMessageElements()` instead.") - val mentionedUsers: MessageMentionedUsers? - - /** - * List of referenced channels with IDs and names. - */ - @Deprecated("Use `Message.getMessageElements()` instead.") - val referencedChannels: MessageReferencedChannels? - - /** - * Checks if the current user added a given emoji to the message. - * - * @param reaction Specific emoji added to the message. - * @return Specifies if the current user added a given emoji to the message or not. - */ - fun hasUserReaction(reaction: String): Boolean - - /** - * Changes the content of the existing message to a new one. - * - * @param newText New/updated text that you want to add in place of the existing message. - * @return An updated message instance with an added `edited` action type. - */ - fun editText(newText: String): PNFuture - - /** - * Either permanently removes a historical message from Message Persistence or marks it as deleted (if you remove the message with the soft option). - * - * Requires Message Persistence configuration. To manage messages, you must enable Message Persistence for your app's keyset in the Admin Portal. To delete messages from PubNub storage, you must also mark the Enable Delete-From-History option. - * - * @param soft Decide if you want to permanently remove message data. By default, the message data gets permanently deleted from Message Persistence. If you set this parameter to true, the Message object gets the deleted status and you can still restore/get its data. - * @param preserveFiles Define if you want to keep the files attached to the message or remove them. - * @return For hard delete, the method returns `PNFuture` without a value (`null`). For soft delete, a `PNFuture` with an updated message instance with an added deleted action type. - */ - fun delete(soft: Boolean = false, preserveFiles: Boolean = false): PNFuture - - /** - * Get the thread channel on which the thread message is published. - * - * @return PNFuture that returns a ThreadChannel object which can be used for sending and reading messages from the message thread. - */ - fun getThread(): PNFuture - - /** - * Forward a given message from one channel to another. - * - * @param channelId Unique identifier of the channel to which you want to forward the message. You can forward a message to the same channel on which it was published or to any other. - * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the forwarded message. - */ - fun forward(channelId: String): PNFuture - - /** - * Attach this message to its channel. - * - * @return `PNFuture` containing the updated channel metadata - */ - fun pin(): PNFuture - - /** - * Flag and report an inappropriate message to the admin. - * - * @param reason Reason for reporting/flagging a given message. - * @return [PNFuture] containing [PNPublishResult] that holds the timetoken of the report message. - */ - fun report(reason: String): PNFuture - /** * Create a thread (channel) for a selected message. * @@ -182,38 +24,22 @@ interface Message { * * @return A pair of values containing an object with details about the result of the remove message action (indicating whether the message was successfully removed and potentially including additional metadata or information about the removal) and the updated channel object after the removal of the thread. */ - fun removeThread(): PNFuture> + fun removeThread(): PNFuture> /** - * Add or remove a reaction to a message. - * - * `toggleReaction()` is a method for both adding and removing message reactions. It adds a string flag to the message if the current user hasn't added it yet or removes it if the current user already added it before. - * - * If you use this method to add or remove message reactions, this flag would be a literal emoji you could implement in your app's UI. However, you could also use this method for a different purpose, like marking a message as pinned to a channel or unpinned if you implement the pinning feature in your chat app. + * Get the thread channel on which the thread message is published. * - * @param reaction Emoji added to the message or removed from it by the current user. - * @return Updated message instance with an added reactions action type. + * @return PNFuture that returns a ThreadChannel object which can be used for sending and reading messages from the message thread. */ - fun toggleReaction(reaction: String): PNFuture + fun getThread(): PNFuture - /** - * You can receive updates when this message and related message reactions are added, edited, or removed. - * - * @param callback Function that takes a single Message object. It defines the custom behavior to be executed when detecting message or message reaction changes. - * @return Interface that lets you stop receiving message-related updates by invoking the close() method - */ - fun streamUpdates(callback: (message: T) -> Unit): AutoCloseable + override fun toggleReaction(reaction: String): PNFuture - /** - * If you delete a message, you can restore its content together with the attached files using the restore() method. - * - * This is possible, however, only if the message you want to restore was soft deleted (the soft parameter was set to true when deleting it). Hard deleted messages cannot be restored as their data is no longer available in Message Persistence. - * - * Requires Message Persistence configuration. To manage messages, you must enable Message Persistence for your app's keyset in the Admin Portal and mark the Enable Delete-From-History option. - * - * @return Object returning the restored Message object. - */ - fun restore(): PNFuture + override fun streamUpdates(callback: (message: Message) -> Unit): AutoCloseable + + override fun restore(): PNFuture + + override fun pin(): PNFuture companion object } diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/MessageDraft.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/MessageDraft.kt index 8daad1fa..7a6b8438 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/MessageDraft.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/MessageDraft.kt @@ -14,7 +14,7 @@ interface MessageDraft { /** * The [Channel] where this [MessageDraft] will be published. */ - val channel: Channel + val channel: BaseChannel<*, *> /** * The scope for searching for suggested users - either [UserSuggestionSource.GLOBAL] or [UserSuggestionSource.CHANNEL]. @@ -39,7 +39,7 @@ interface MessageDraft { /** * Can be used to set a [Message] to quote when sending this [MessageDraft]. */ - var quotedMessage: Message? + var quotedMessage: BaseMessage<*, *>? /** * Can be used to attach files to send with this [MessageDraft]. diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadChannel.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadChannel.kt index f28bfd86..ef3bd051 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadChannel.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadChannel.kt @@ -6,7 +6,7 @@ import com.pubnub.kmp.PNFuture /** * Represents an object that refers to a single thread (channel) in a chat. */ -interface ThreadChannel : Channel { +interface ThreadChannel : BaseChannel { /** * Message for which the thread was created. */ @@ -17,32 +17,10 @@ interface ThreadChannel : Channel { */ val parentChannelId: String - /** - * Pins a selected thread message to the thread channel. - * - * @param message you want to pin to the selected thread channel. - * - * @return [PNFuture] containing [ThreadChannel] - */ - override fun pinMessage(message: Message): PNFuture + override fun pinMessage(message: BaseMessage<*, *>): PNFuture - /** - * Unpins the previously pinned thread message from the thread channel. - * - * @return [PNFuture] containing [ThreadChannel] - */ override fun unpinMessage(): PNFuture - /** - * Returns historical messages for the [ThreadChannel] - * - * @param startTimetoken - * @param endTimetoken - * @param count The maximum number of messages to retrieve. Default and maximum values is 25. - * - * @return [PNFuture] containing a list of messages with pagination information (isMore: Boolean). The result of - * this future can be processed using the `async` method of `PNFuture`. - */ override fun getHistory(startTimetoken: Long?, endTimetoken: Long?, count: Int): PNFuture> /** diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadMessage.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadMessage.kt index 2c42ddc1..c94aef06 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadMessage.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/ThreadMessage.kt @@ -5,7 +5,7 @@ import com.pubnub.kmp.PNFuture /** * Represents a single message in a thread. */ -interface ThreadMessage : Message { +interface ThreadMessage : BaseMessage { /** * Unique identifier of the main channel on which you create a subchannel (thread channel) and thread messages. */ @@ -31,5 +31,13 @@ interface ThreadMessage : Message { */ fun unpinFromParentChannel(): PNFuture + override fun toggleReaction(reaction: String): PNFuture + + override fun streamUpdates(callback: (message: ThreadMessage) -> Unit): AutoCloseable + + override fun restore(): PNFuture + + override fun pin(): PNFuture + companion object } diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/User.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/User.kt index 108c9a8e..b4cf4715 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/User.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/User.kt @@ -179,7 +179,7 @@ interface User { * @return [PNFuture] indicating the result of setting the restriction. */ fun setRestrictions( - channel: Channel, + channel: BaseChannel<*, *>, ban: Boolean = false, mute: Boolean = false, reason: String? = null, @@ -191,7 +191,7 @@ interface User { * @param channel The [Channel] for which to retrieve the restrictions. * @return [PNFuture] containing the [Restriction] applied to the user in the specified channel. */ - fun getChannelRestrictions(channel: Channel): PNFuture + fun getChannelRestrictions(channel: BaseChannel<*, *>): PNFuture /** * Retrieves all restrictions applied to the user on all channels they are a member of. diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt index c720933f..c934a630 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt @@ -74,7 +74,7 @@ interface ChatConfiguration { * Specifically, the data is saved in the `custom` object of the following User in App Context: * * ``` - * PN_PRIV.{userId}.mute.1 + * PN_PRIV..mute.1 * ``` * * where {userId} is the current [com.pubnub.api.v2.PNConfiguration.userId]. diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/HistoryResponse.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/HistoryResponse.kt index 767c958b..c6fc1323 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/HistoryResponse.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/HistoryResponse.kt @@ -1,15 +1,15 @@ package com.pubnub.chat.types -import com.pubnub.chat.Message +import com.pubnub.chat.BaseMessage /** * Represents the response returned when fetching the historical messages for a [Channel]. * - * @param T The type of messages contained in the history response. - * @property messages A list of messages of type [T] retrieved from the channel history. + * @param M The type of messages contained in the history response. + * @property messages A list of messages of type [M] retrieved from the channel history. * @property isMore A boolean indicating whether there are more messages available beyond the current result set. */ -class HistoryResponse( - val messages: List, +class HistoryResponse>( + val messages: List, val isMore: Boolean, ) diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/UserMentionData.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/UserMentionData.kt index 325c3792..be3def04 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/UserMentionData.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/UserMentionData.kt @@ -1,7 +1,9 @@ package com.pubnub.chat.types +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Event import com.pubnub.chat.Message +import com.pubnub.chat.ThreadMessage /** * A sealed class representing the data related to a user mention event. @@ -12,7 +14,7 @@ import com.pubnub.chat.Message */ sealed class UserMentionData { abstract val event: Event - abstract val message: Message + abstract val message: BaseMessage<*, *> abstract val userId: String } @@ -42,7 +44,7 @@ class ChannelMentionData( */ class ThreadMentionData( override val event: Event, - override val message: Message, + override val message: ThreadMessage, override val userId: String, val parentChannelId: String, val threadChannelId: String diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt index 8010c288..f39c32e0 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt @@ -17,6 +17,7 @@ import com.pubnub.api.models.consumer.objects.PNKey import com.pubnub.api.models.consumer.objects.PNMembershipKey import com.pubnub.api.models.consumer.objects.PNPage import com.pubnub.api.models.consumer.objects.PNSortKey +import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadataResult import com.pubnub.api.models.consumer.objects.member.PNMember import com.pubnub.api.models.consumer.objects.member.PNMemberArrayResult @@ -34,17 +35,20 @@ import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult import com.pubnub.api.utils.Clock import com.pubnub.api.utils.Instant import com.pubnub.api.v2.callbacks.Result +import com.pubnub.chat.BaseChannel +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel import com.pubnub.chat.Chat import com.pubnub.chat.Event import com.pubnub.chat.Membership import com.pubnub.chat.Message import com.pubnub.chat.ThreadChannel +import com.pubnub.chat.ThreadMessage import com.pubnub.chat.User import com.pubnub.chat.config.ChatConfiguration import com.pubnub.chat.config.LogLevel import com.pubnub.chat.config.PushNotificationsConfig -import com.pubnub.chat.internal.channel.BaseChannel +import com.pubnub.chat.internal.channel.BaseChannelImpl import com.pubnub.chat.internal.channel.ChannelImpl import com.pubnub.chat.internal.channel.ThreadChannelImpl import com.pubnub.chat.internal.error.PubNubErrorMessage @@ -64,17 +68,16 @@ import com.pubnub.chat.internal.error.PubNubErrorMessage.FAILED_TO_RETRIEVE_WHO_ import com.pubnub.chat.internal.error.PubNubErrorMessage.FAILED_TO_SOFT_DELETE_CHANNEL import com.pubnub.chat.internal.error.PubNubErrorMessage.ID_IS_REQUIRED import com.pubnub.chat.internal.error.PubNubErrorMessage.MODERATION_CAN_BE_SET_ONLY_BY_CLIENT_HAVING_SECRET_KEY -import com.pubnub.chat.internal.error.PubNubErrorMessage.ONLY_ONE_LEVEL_OF_THREAD_NESTING_IS_ALLOWED import com.pubnub.chat.internal.error.PubNubErrorMessage.STORE_USER_ACTIVITY_INTERVAL_SHOULD_BE_AT_LEAST_1_MIN import com.pubnub.chat.internal.error.PubNubErrorMessage.THERE_IS_NO_ACTION_TIMETOKEN_CORRESPONDING_TO_THE_THREAD import com.pubnub.chat.internal.error.PubNubErrorMessage.THERE_IS_NO_THREAD_TO_BE_DELETED import com.pubnub.chat.internal.error.PubNubErrorMessage.THERE_IS_NO_THREAD_WITH_ID import com.pubnub.chat.internal.error.PubNubErrorMessage.THIS_MESSAGE_IS_NOT_A_THREAD import com.pubnub.chat.internal.error.PubNubErrorMessage.THIS_THREAD_ID_ALREADY_RESTORED -import com.pubnub.chat.internal.error.PubNubErrorMessage.THREAD_FOR_THIS_MESSAGE_ALREADY_EXISTS import com.pubnub.chat.internal.error.PubNubErrorMessage.USER_ID_ALREADY_EXIST import com.pubnub.chat.internal.error.PubNubErrorMessage.USER_NOT_EXIST -import com.pubnub.chat.internal.error.PubNubErrorMessage.YOU_CAN_NOT_CREATE_THREAD_ON_DELETED_MESSAGES +import com.pubnub.chat.internal.message.MessageImpl +import com.pubnub.chat.internal.message.ThreadMessageImpl import com.pubnub.chat.internal.mutelist.MutedUsersManagerImpl import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.internal.timer.PlatformTimer @@ -126,7 +129,7 @@ class ChatImpl( ?: MessageActionType.DELETED.toString(), override val reactionsActionName: String = config.customPayloads?.reactionsActionName ?: MessageActionType.REACTIONS.toString(), - override val timerManager: TimerManager = createTimerManager() + override val timerManager: TimerManager = createTimerManager(), ) : ChatInternal { override var currentUser: User = UserImpl(this, pubNub.configuration.userId.value, name = pubNub.configuration.userId.value) @@ -238,11 +241,21 @@ class ChatImpl( } } + fun > getChannel(channelId: String, channelFactory: (ChatInternal, PNChannelMetadata) -> C): PNFuture { + if (!isValidId(channelId)) { + return log.logErrorAndReturnException(CHANNEL_ID_IS_REQUIRED).asFuture() + } + return pubNub.getChannelMetadata(channel = channelId, includeCustom = true) + .then { pnChannelMetadataResult: PNChannelMetadataResult -> + channelFactory(this, pnChannelMetadataResult.data) + }.nullOn404() + } + override fun removeThreadChannel( chat: Chat, message: Message, - soft: Boolean - ): PNFuture> { + soft: Boolean, + ): PNFuture> { if (!message.hasThread) { return PubNubException(THERE_IS_NO_THREAD_TO_BE_DELETED).logErrorAndReturnException(log).asFuture() } @@ -255,15 +268,25 @@ class ChatImpl( log ).asFuture() - return chat.getChannel(threadId).thenAsync { threadChannel -> - if (threadChannel == null) { - log.pnError("$THERE_IS_NO_THREAD_WITH_ID$threadId") + return getChannel( + channelId = threadId, + channelFactory = { chat, data -> ThreadChannelImpl.fromDTO(chat, message, data) } + ) + .thenAsync { threadChannel -> + if (threadChannel == null) { + log.pnError("$THERE_IS_NO_THREAD_WITH_ID$threadId") + } + awaitAll( + chat.pubNub.removeMessageAction(message.channelId, message.timetoken, actionTimetoken), + performDeleteChannel(threadChannel.id, soft).then { + if (it != null) { + threadChannel + it.data + } else { + null + } + } + ) } - awaitAll( - chat.pubNub.removeMessageAction(message.channelId, message.timetoken, actionTimetoken), - threadChannel.delete(soft) - ) - } } override fun getUser(userId: String): PNFuture { @@ -313,7 +336,7 @@ class ChatImpl( email: String?, custom: CustomObject?, status: String?, - type: String? + type: String?, ): PNFuture { if (!isValidId(id)) { return log.logErrorAndReturnException(ID_IS_REQUIRED).asFuture() @@ -383,8 +406,21 @@ class ChatImpl( description: String?, custom: CustomObject?, type: ChannelType?, - status: String? + status: String?, ): PNFuture { + return performCreateChannel(id, name, description, custom, type, status).then { + ChannelImpl.fromDTO(this, it.data) + } + } + + override fun performCreateChannel( + id: String, + name: String?, + description: String?, + custom: CustomObject?, + type: ChannelType?, + status: String?, + ): PNFuture { if (!isValidId(id)) { return log.logErrorAndReturnException(CHANNEL_ID_IS_REQUIRED).asFuture() } @@ -401,7 +437,7 @@ class ChatImpl( filter: String?, sort: Collection>, limit: Int?, - page: PNPage? + page: PNPage?, ): PNFuture { return pubNub.getAllChannelMetadata( limit = limit, @@ -441,7 +477,7 @@ class ChatImpl( custom: CustomObject?, description: String?, status: String?, - type: ChannelType? + type: ChannelType?, ): PNFuture { if (!isValidId(id)) { return log.logErrorAndReturnException(CHANNEL_ID_IS_REQUIRED).asFuture() @@ -449,7 +485,9 @@ class ChatImpl( return getChannel(id).thenAsync { channel: Channel? -> if (channel != null) { - setChannelMetadata(id, name, description, custom, type, status) + setChannelMetadata(id, name, description, custom, type, status).then { pnChannelMetadataResult -> + ChannelImpl.fromDTO(this, pnChannelMetadataResult.data) + } } else { log.pnError(CHANNEL_NOT_FOUND) } @@ -457,6 +495,12 @@ class ChatImpl( } override fun deleteChannel(id: String, soft: Boolean): PNFuture { + return performDeleteChannel(id, soft).then { result -> + result?.let { ChannelImpl.fromDTO(this, it.data) } + } + } + + override fun performDeleteChannel(id: String, soft: Boolean): PNFuture { if (!isValidId(id)) { return log.logErrorAndReturnException(CHANNEL_ID_IS_REQUIRED).asFuture() } @@ -469,11 +513,11 @@ class ChatImpl( performSoftChannelDelete(channel) } } else { - performChannelDelete(id).then { null } + performHardChannelDelete(id).then { null } } } - override fun forwardMessage(message: Message, channelId: String): PNFuture { + override fun forwardMessage(message: BaseMessage<*, *>, channelId: String): PNFuture { if (!isValidId(channelId)) { return log.logErrorAndReturnException(CHANNEL_ID_IS_REQUIRED).asFuture() } @@ -497,7 +541,7 @@ class ChatImpl( override fun emitEvent( channelId: String, payload: T, - mergePayloadWith: Map? + mergePayloadWith: Map?, ): PNFuture { val emitMethod = payload::class.getEmitMethod() ?: (payload as? EventContent.Custom)?.method return if (emitMethod == EmitEventMethod.SIGNAL) { @@ -520,7 +564,7 @@ class ChatImpl( channelName: String?, channelDescription: String?, channelCustom: CustomObject?, - channelStatus: String? + channelStatus: String?, ): PNFuture { val finalChannelId: String = channelId ?: generateRandomUuid() @@ -595,7 +639,7 @@ class ChatImpl( channelDescription: String?, channelCustom: CustomObject?, channelStatus: String?, - membershipCustom: CustomObject? + membershipCustom: CustomObject?, ): PNFuture { val user = this.currentUser val finalChannelId = channelId ?: generateRandomUuid() @@ -655,7 +699,7 @@ class ChatImpl( type: KClass, channelId: String, customMethod: EmitEventMethod, - callback: (event: Event) -> Unit + callback: (event: Event) -> Unit, ): AutoCloseable { val handler = fun(_: PubNub, pnEvent: PNEvent) { try { @@ -717,7 +761,7 @@ class ChatImpl( } override fun setRestrictions( - restriction: Restriction + restriction: Restriction, ): PNFuture { if (this.pubNub.configuration.secretKey.isEmpty()) { return log.logErrorAndReturnException(MODERATION_CAN_BE_SET_ONLY_BY_CLIENT_HAVING_SECRET_KEY).asFuture() @@ -835,7 +879,7 @@ class ChatImpl( limit: Int?, page: PNPage?, filter: String?, - sort: Collection> + sort: Collection>, ): PNFuture> { return currentUser.getMemberships(limit = limit, page = page, filter = filter, sort = sort) .thenAsync { membershipsResponse: MembershipsResponse -> @@ -991,7 +1035,7 @@ class ChatImpl( channelId: String, startTimetoken: Long?, endTimetoken: Long?, - count: Int + count: Int, ): PNFuture { return pubNub.fetchMessages( channels = listOf(channelId), @@ -1022,7 +1066,7 @@ class ChatImpl( override fun getCurrentUserMentions( startTimetoken: Long?, endTimetoken: Long?, - count: Int + count: Int, ): PNFuture { if (count > 100) { return log.logErrorAndReturnException(COUNT_SHOULD_NOT_EXCEED_100).asFuture() @@ -1042,35 +1086,54 @@ class ChatImpl( .map { mentionEvent: Event -> val mentionTimetoken = mentionEvent.payload.messageTimetoken val mentionChannelId = mentionEvent.payload.channel - - BaseChannel.getMessage(chat = this, channelId = mentionChannelId, timetoken = mentionTimetoken) - .then { message: Message? -> - if (message == null) { - return@then null + val mentionParentChannelId = mentionEvent.payload.parentChannel + + if (mentionParentChannelId != null) { + BaseChannelImpl.getMessage( + chat = this, + channelId = mentionChannelId, + timetoken = mentionTimetoken, + messageFactory = { chat, item, messageFactory -> + ThreadMessageImpl.fromDTO(chat, item, mentionChannelId, mentionParentChannelId) } - if (mentionEvent.payload.parentChannel == null) { - ChannelMentionData( + ).then { message: ThreadMessage? -> + if (message == null) { + null + } else { + ThreadMentionData( event = mentionEvent, message = message, userId = mentionEvent.userId, - channelId = mentionChannelId + parentChannelId = mentionParentChannelId, + threadChannelId = mentionChannelId ) + } + } + } else { + BaseChannelImpl.getMessage( + this, + mentionChannelId, + mentionTimetoken, + MessageImpl::fromDTO + ).then { message -> + if (message == null) { + null } else { - ThreadMentionData( + ChannelMentionData( event = mentionEvent, message = message, userId = mentionEvent.userId, - parentChannelId = mentionEvent.payload.parentChannel.orEmpty(), - threadChannelId = mentionEvent.payload.channel + channelId = mentionChannelId ) } } + } }.awaitAll() + .then { it.filterNotNull() } + .then { userMentionDataList: List -> + GetCurrentUserMentionsResult(enhancedMentionsData = userMentionDataList, isMore = isMore) + } } - .then { it.filterNotNull() } - .then { userMentionDataList: List -> - GetCurrentUserMentionsResult(enhancedMentionsData = userMentionDataList, isMore = isMore) - } } override fun destroy() { @@ -1080,7 +1143,7 @@ class ChatImpl( private fun getTimetokenFromHistoryMessage( channelId: String, - pnFetchMessagesResult: PNFetchMessagesResult + pnFetchMessagesResult: PNFetchMessagesResult, ): Long { val relevantLastMessage: List? = pnFetchMessagesResult.channelsUrlDecoded[channelId] return relevantLastMessage?.firstOrNull()?.timetoken ?: 0 @@ -1113,8 +1176,8 @@ class ChatImpl( private fun performUserDelete(userId: String): PNFuture = pubNub.removeUUIDMetadata(uuid = userId).then { } - private fun performSoftChannelDelete(channel: Channel): PNFuture { - val updatedChannel = (channel as BaseChannel<*, *>).copyWithStatusDeleted() + private fun performSoftChannelDelete(channel: BaseChannel<*, *>): PNFuture { + val updatedChannel = (channel as BaseChannelImpl<*, *>).copyWithStatusDeleted() return pubNub.setChannelMetadata( channel = channel.id, name = updatedChannel.name, @@ -1123,24 +1186,22 @@ class ChatImpl( includeCustom = false, type = updatedChannel.type?.stringValue, status = updatedChannel.status - ).then { pnChannelMetadataResult -> - ChannelImpl.fromDTO(this, pnChannelMetadataResult.data) - }.catch { exception -> + ).catch { exception -> Result.failure(PubNubException(FAILED_TO_SOFT_DELETE_CHANNEL, exception)) } } - private fun performChannelDelete(channelId: String): PNFuture = + private fun performHardChannelDelete(channelId: String): PNFuture = pubNub.removeChannelMetadata(channel = channelId).then { } - private fun setChannelMetadata( + override fun setChannelMetadata( id: String, name: String?, description: String?, custom: CustomObject?, type: ChannelType?, status: String?, - ): PNFuture { + ): PNFuture { return pubNub.setChannelMetadata( channel = id, name = name, @@ -1149,9 +1210,7 @@ class ChatImpl( includeCustom = true, type = type?.stringValue, status = status - ).then { pnChannelMetadataResult -> - ChannelImpl.fromDTO(this, pnChannelMetadataResult.data) - }.catch { exception -> + ).catch { exception -> Result.failure(PubNubException(FAILED_TO_CREATE_UPDATE_CHANNEL_DATA, exception)) } } @@ -1188,8 +1247,8 @@ class ChatImpl( internal fun pinOrUnpinMessageToChannel( pubNub: PubNub, - message: Message?, - channel: Channel + message: BaseMessage<*, *>?, + channel: BaseChannel<*, *>, ): PNFuture { val customMetadataToSet = channel.custom?.toMutableMap() ?: mutableMapOf() if (message == null) { @@ -1208,32 +1267,16 @@ class ChatImpl( ) } - internal fun getThreadId(channelId: String, messageTimetoken: Long): String { + fun getThreadId(channelId: String, messageTimetoken: Long): String { return "${MESSAGE_THREAD_ID_PREFIX}_${channelId}_$messageTimetoken" } - internal fun createThreadChannel(chat: ChatInternal, message: Message): PNFuture { - if (message.channelId.startsWith(MESSAGE_THREAD_ID_PREFIX)) { - return log.logErrorAndReturnException(ONLY_ONE_LEVEL_OF_THREAD_NESTING_IS_ALLOWED).asFuture() - } - if (message.deleted) { - return log.logErrorAndReturnException(YOU_CAN_NOT_CREATE_THREAD_ON_DELETED_MESSAGES).asFuture() - } + fun getParentChannelIdFromThreadId(threadChannelId: String): String { + return threadChannelId.removePrefix(MESSAGE_THREAD_ID_PREFIX + "_").substringBeforeLast("_") + } - val threadChannelId = - getThreadId(message.channelId, message.timetoken) - return chat.getChannel(threadChannelId).thenAsync { it: Channel? -> - if (it != null) { - return@thenAsync log.logErrorAndReturnException(THREAD_FOR_THIS_MESSAGE_ALREADY_EXISTS).asFuture() - } - ThreadChannelImpl( - message, - chat, - description = "Thread on channel ${message.channelId} with message timetoken ${message.timetoken}", - id = threadChannelId, - threadCreated = false - ).asFuture() - } + fun getParentMessageTimetokenFromThreadId(threadChannelId: String): Long { + return threadChannelId.substringAfterLast("_").toLong() } } diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt index 630a685d..75e1f9c4 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt @@ -3,6 +3,8 @@ package com.pubnub.chat.internal import com.pubnub.api.models.consumer.PNPublishResult import com.pubnub.api.models.consumer.message_actions.PNMessageAction import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult +import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadataResult +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel import com.pubnub.chat.Chat import com.pubnub.chat.Message @@ -25,7 +27,7 @@ interface ChatInternal : Chat { chat: Chat, message: Message, soft: Boolean = false - ): PNFuture> + ): PNFuture> fun restoreThreadChannel(message: Message): PNFuture @@ -38,7 +40,7 @@ interface ChatInternal : Chat { status: String? = null, ): PNFuture - fun forwardMessage(message: Message, channelId: String): PNFuture + fun forwardMessage(message: BaseMessage<*, *>, channelId: String): PNFuture fun getThreadChannel(message: Message): PNFuture @@ -63,4 +65,24 @@ interface ChatInternal : Chat { * @return [PNFuture] containing set of [User] */ fun getUserSuggestions(text: String, limit: Int = 10): PNFuture> + + fun setChannelMetadata( + id: String, + name: String?, + description: String?, + custom: CustomObject?, + type: ChannelType?, + status: String?, + ): PNFuture + + fun performDeleteChannel(id: String, soft: Boolean): PNFuture + + fun performCreateChannel( + id: String, + name: String?, + description: String?, + custom: CustomObject?, + type: ChannelType?, + status: String? + ): PNFuture } diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MembershipImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MembershipImpl.kt index 813a4ee6..18773915 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MembershipImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MembershipImpl.kt @@ -7,9 +7,9 @@ import com.pubnub.api.models.consumer.objects.membership.PNChannelMembership import com.pubnub.api.models.consumer.pubsub.objects.PNDeleteMembershipEventMessage import com.pubnub.api.models.consumer.pubsub.objects.PNSetMembershipEvent import com.pubnub.api.models.consumer.pubsub.objects.PNSetMembershipEventMessage +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel import com.pubnub.chat.Membership -import com.pubnub.chat.Message import com.pubnub.chat.User import com.pubnub.chat.internal.channel.ChannelImpl import com.pubnub.chat.internal.error.PubNubErrorMessage.CAN_NOT_STREAM_MEMBERSHIP_UPDATES_ON_EMPTY_LIST @@ -43,7 +43,7 @@ data class MembershipImpl( return custom?.get(METADATA_LAST_READ_MESSAGE_TIMETOKEN).tryLong() } - override fun setLastReadMessage(message: Message): PNFuture { + override fun setLastReadMessage(message: BaseMessage<*, *>): PNFuture { return setLastReadMessageTimetoken(message.timetoken) } diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MessageDraftImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MessageDraftImpl.kt index c2def44b..8ec5bbc9 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MessageDraftImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/MessageDraftImpl.kt @@ -2,9 +2,10 @@ package com.pubnub.chat.internal import co.touchlab.kermit.Logger import com.pubnub.api.models.consumer.PNPublishResult +import com.pubnub.chat.BaseChannel +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel import com.pubnub.chat.MentionTarget -import com.pubnub.chat.Message import com.pubnub.chat.MessageDraft import com.pubnub.chat.MessageDraftChangeListener import com.pubnub.chat.MessageElement @@ -32,13 +33,13 @@ private const val SCHEMA_CHANNEL = "pn-channel://" * from a single thread at a time. */ class MessageDraftImpl( - override val channel: Channel, + override val channel: BaseChannel<*, *>, override val userSuggestionSource: MessageDraft.UserSuggestionSource = MessageDraft.UserSuggestionSource.CHANNEL, override val isTypingIndicatorTriggered: Boolean = channel.type != ChannelType.PUBLIC, override val userLimit: Int = 10, override val channelLimit: Int = 10 ) : MessageDraft { - override var quotedMessage: Message? = null + override var quotedMessage: BaseMessage<*, *>? = null set(value) { if (value != null && value.channelId != this.channel.id) { log.pnError(PubNubErrorMessage.CANNOT_QUOTE_MESSAGE_FROM_OTHER_CHANNELS) diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/UserImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/UserImpl.kt index f9518035..dbbb65a3 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/UserImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/UserImpl.kt @@ -17,7 +17,7 @@ import com.pubnub.api.utils.Clock import com.pubnub.api.utils.Instant import com.pubnub.api.utils.PatchValue import com.pubnub.api.v2.callbacks.Result -import com.pubnub.chat.Channel +import com.pubnub.chat.BaseChannel import com.pubnub.chat.Membership import com.pubnub.chat.User import com.pubnub.chat.internal.error.PubNubErrorMessage @@ -140,7 +140,7 @@ data class UserImpl( } } - override fun setRestrictions(channel: Channel, ban: Boolean, mute: Boolean, reason: String?): PNFuture { + override fun setRestrictions(channel: BaseChannel<*, *>, ban: Boolean, mute: Boolean, reason: String?): PNFuture { if (chat.pubNub.configuration.secretKey.isEmpty()) { return log.logErrorAndReturnException(MODERATION_CAN_BE_SET_ONLY_BY_CLIENT_HAVING_SECRET_KEY).asFuture() } @@ -155,7 +155,7 @@ data class UserImpl( ) } - override fun getChannelRestrictions(channel: Channel): PNFuture { + override fun getChannelRestrictions(channel: BaseChannel<*, *>): PNFuture { return getRestrictions(channel).then { pnChannelMembershipArrayResult -> val firstMembership: PNChannelMembership = pnChannelMembershipArrayResult.data.first() RestrictionImpl.fromChannelMembershipDTO(id, firstMembership) @@ -203,7 +203,7 @@ data class UserImpl( } internal fun getRestrictions( - channel: Channel?, + channel: BaseChannel<*, *>?, limit: Int? = null, page: PNPage? = null, sort: Collection> = listOf(), diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/BaseChannel.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/BaseChannelImpl.kt similarity index 75% rename from pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/BaseChannel.kt rename to pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/BaseChannelImpl.kt index 3c09829e..893dc083 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/BaseChannel.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/BaseChannelImpl.kt @@ -5,7 +5,6 @@ import com.pubnub.api.PubNubException import com.pubnub.api.endpoints.objects.member.GetChannelMembers import com.pubnub.api.models.consumer.PNBoundedPage import com.pubnub.api.models.consumer.PNPublishResult -import com.pubnub.api.models.consumer.PNTimeResult import com.pubnub.api.models.consumer.files.PNDeleteFileResult import com.pubnub.api.models.consumer.files.PNFileUrlResult import com.pubnub.api.models.consumer.history.PNFetchMessageItem @@ -15,11 +14,8 @@ import com.pubnub.api.models.consumer.objects.PNPage import com.pubnub.api.models.consumer.objects.PNSortKey import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata import com.pubnub.api.models.consumer.objects.member.MemberInclude -import com.pubnub.api.models.consumer.objects.member.PNMember import com.pubnub.api.models.consumer.objects.member.PNMemberArrayResult -import com.pubnub.api.models.consumer.objects.membership.MembershipInclude -import com.pubnub.api.models.consumer.objects.membership.PNChannelMembership -import com.pubnub.api.models.consumer.objects.membership.PNChannelMembershipArrayResult +import com.pubnub.api.models.consumer.pubsub.PNMessageResult import com.pubnub.api.models.consumer.pubsub.objects.PNDeleteChannelMetadataEventMessage import com.pubnub.api.models.consumer.pubsub.objects.PNObjectEventResult import com.pubnub.api.models.consumer.pubsub.objects.PNSetChannelMetadataEventMessage @@ -29,45 +25,38 @@ import com.pubnub.api.utils.Instant import com.pubnub.api.utils.PatchValue import com.pubnub.api.v2.callbacks.Result import com.pubnub.api.v2.subscriptions.SubscriptionOptions +import com.pubnub.chat.BaseChannel +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel import com.pubnub.chat.Event -import com.pubnub.chat.Membership -import com.pubnub.chat.Message import com.pubnub.chat.User import com.pubnub.chat.config.PushNotificationsConfig import com.pubnub.chat.internal.ChatImpl.Companion.pinOrUnpinMessageToChannel import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.INTERNAL_MODERATION_PREFIX -import com.pubnub.chat.internal.METADATA_LAST_READ_MESSAGE_TIMETOKEN import com.pubnub.chat.internal.METADATA_MENTIONED_USERS import com.pubnub.chat.internal.METADATA_QUOTED_MESSAGE import com.pubnub.chat.internal.METADATA_REFERENCED_CHANNELS import com.pubnub.chat.internal.METADATA_TEXT_LINKS import com.pubnub.chat.internal.MINIMAL_TYPING_INDICATOR_TIMEOUT -import com.pubnub.chat.internal.MembershipImpl import com.pubnub.chat.internal.PINNED_MESSAGE_CHANNEL_ID import com.pubnub.chat.internal.PINNED_MESSAGE_TIMETOKEN import com.pubnub.chat.internal.defaultGetMessageResponseBody import com.pubnub.chat.internal.error.PubNubErrorMessage.CANNOT_QUOTE_MESSAGE_FROM_OTHER_CHANNELS import com.pubnub.chat.internal.error.PubNubErrorMessage.CAN_NOT_STREAM_CHANNEL_UPDATES_ON_EMPTY_LIST -import com.pubnub.chat.internal.error.PubNubErrorMessage.CHANNEL_INVITES_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS import com.pubnub.chat.internal.error.PubNubErrorMessage.ERROR_HANDLING_ONMESSAGE_EVENT import com.pubnub.chat.internal.error.PubNubErrorMessage.FAILED_TO_RETRIEVE_HISTORY_DATA import com.pubnub.chat.internal.error.PubNubErrorMessage.MODERATION_CAN_BE_SET_ONLY_BY_CLIENT_HAVING_SECRET_KEY -import com.pubnub.chat.internal.error.PubNubErrorMessage.READ_RECEIPTS_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS import com.pubnub.chat.internal.error.PubNubErrorMessage.THREAD_CHANNEL_DOES_NOT_EXISTS import com.pubnub.chat.internal.error.PubNubErrorMessage.TYPING_INDICATORS_NO_SUPPORTED_IN_PUBLIC_CHATS -import com.pubnub.chat.internal.message.BaseMessage -import com.pubnub.chat.internal.message.MessageImpl +import com.pubnub.chat.internal.message.BaseMessageImpl import com.pubnub.chat.internal.restrictions.RestrictionImpl import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.internal.util.channelsUrlDecoded import com.pubnub.chat.internal.util.logErrorAndReturnException import com.pubnub.chat.internal.util.pnError import com.pubnub.chat.internal.utils.ExponentialRateLimiter -import com.pubnub.chat.internal.uuidFilterString import com.pubnub.chat.listenForEvents -import com.pubnub.chat.membership.MembersResponse import com.pubnub.chat.restrictions.GetRestrictionsResponse import com.pubnub.chat.restrictions.Restriction import com.pubnub.chat.types.ChannelType @@ -78,14 +67,12 @@ import com.pubnub.chat.types.GetFileItem import com.pubnub.chat.types.GetFilesResult import com.pubnub.chat.types.HistoryResponse import com.pubnub.chat.types.InputFile -import com.pubnub.chat.types.JoinResult import com.pubnub.chat.types.MessageMentionedUsers import com.pubnub.chat.types.MessageReferencedChannel import com.pubnub.chat.types.MessageReferencedChannels import com.pubnub.chat.types.TextLink import com.pubnub.kmp.CustomObject import com.pubnub.kmp.PNFuture -import com.pubnub.kmp.alsoAsync import com.pubnub.kmp.asFuture import com.pubnub.kmp.awaitAll import com.pubnub.kmp.catch @@ -101,7 +88,7 @@ import tryLong import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -abstract class BaseChannel( +abstract class BaseChannelImpl, M : BaseMessage>( override val chat: ChatInternal, private val clock: Clock = Clock.System, override val id: String, @@ -113,8 +100,8 @@ abstract class BaseChannel( override val type: ChannelType? = null, val channelFactory: (ChatInternal, PNChannelMetadata) -> C, val messageFactory: (ChatInternal, PNFetchMessageItem, channelId: String) -> M, -) : Channel { - private val suggestedMemberships = mutableMapOf>() + val messageFactory2: (ChatInternal, PNMessageResult) -> M, +) : BaseChannel { internal var typingSent: Instant? = null private val sendTextRateLimiter by lazy { ExponentialRateLimiter( @@ -123,7 +110,7 @@ abstract class BaseChannel( chat.timerManager ) } - private val channelFilterString get() = "channel.id == '${this.id}'" + private val typingTimeout get() = maxOf(chat.config.typingTimeout, MINIMAL_TYPING_INDICATOR_TIMEOUT) private val typingTimoutMargin = 500.milliseconds // sendTypingSignal 500 millis before typingTimeout expires to ensure continuity @@ -133,15 +120,19 @@ abstract class BaseChannel( description: String?, status: String?, type: ChannelType?, - ): PNFuture { - return chat.updateChannel(id, name, custom, description, status, type) + ): PNFuture { + return chat.setChannelMetadata(id, name, description, custom, type, status).then { pnChannelMetadataResult -> + channelFactory(chat, pnChannelMetadataResult.data) + } } - override fun delete(soft: Boolean): PNFuture { - return chat.deleteChannel(id, soft) + override fun delete(soft: Boolean): PNFuture { + return chat.performDeleteChannel(id, soft).then { result -> + result?.let { channelFactory(chat, it.data) } + } } - override fun forwardMessage(message: Message): PNFuture { + override fun forwardMessage(message: BaseMessage<*, *>): PNFuture { return chat.forwardMessage(message, this.id) } @@ -233,8 +224,8 @@ abstract class BaseChannel( return chat.isPresent(userId, id) } - override fun streamUpdates(callback: (channel: Channel?) -> Unit): AutoCloseable { - return streamUpdatesOn(listOf(this)) { + override fun streamUpdates(callback: (C?) -> Unit): AutoCloseable { + return streamUpdatesOn(listOf(this as C), channelFactory) { callback(it.firstOrNull()) } } @@ -242,7 +233,7 @@ abstract class BaseChannel( override fun getHistory( startTimetoken: Long?, endTimetoken: Long?, - count: Int, + count: Int ): PNFuture> { return getHistory( chat = chat, @@ -264,7 +255,7 @@ abstract class BaseChannel( mentionedUsers: MessageMentionedUsers?, referencedChannels: Map?, textLinks: List?, - quotedMessage: Message?, + quotedMessage: BaseMessage<*, *>?, files: List?, customPushData: Map?, ): PNFuture { @@ -278,7 +269,7 @@ abstract class BaseChannel( shouldStore: Boolean, usePost: Boolean, ttl: Int?, - quotedMessage: Message?, + quotedMessage: BaseMessage<*, *>?, files: List?, usersToMention: Collection?, customPushData: Map?, @@ -302,7 +293,7 @@ abstract class BaseChannel( shouldStore: Boolean, usePost: Boolean, ttl: Int?, - quotedMessage: Message?, + quotedMessage: BaseMessage<*, *>?, files: List?, usersToMention: Collection? = null, customPushData: Map? = null, @@ -348,7 +339,7 @@ abstract class BaseChannel( private fun buildMetaForPublish( meta: Map?, - quotedMessage: Message?, + quotedMessage: BaseMessage<*, *>?, mentionedUsers: MessageMentionedUsers? = null, referencedChannels: MessageReferencedChannels? = null, textLinks: List? = null, @@ -357,7 +348,7 @@ abstract class BaseChannel( quotedMessage?.let { put( METADATA_QUOTED_MESSAGE, - PNDataEncoder.encode((quotedMessage as BaseMessage<*>).asQuotedMessage())!! + PNDataEncoder.encode((quotedMessage as BaseMessageImpl<*, *>).asQuotedMessage())!! ) } mentionedUsers?.let { put(METADATA_MENTIONED_USERS, PNDataEncoder.encode(it)!!) } @@ -365,109 +356,7 @@ abstract class BaseChannel( textLinks?.let { put(METADATA_TEXT_LINKS, PNDataEncoder.encode(it)!!) } } - override fun invite(user: User): PNFuture { - if (this.type == ChannelType.PUBLIC) { - return log.logErrorAndReturnException(CHANNEL_INVITES_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS).asFuture() - } - return getMembers(filter = user.uuidFilterString).thenAsync { channelMembers: MembersResponse -> - if (channelMembers.members.isNotEmpty()) { - return@thenAsync channelMembers.members.first().asFuture() - } else { - chat.pubNub.setMemberships( - channels = listOf(PNChannelMembership.Partial(this.id)), - userId = user.id, - filter = channelFilterString, - include = MembershipInclude( - includeCustom = true, - includeStatus = false, - includeType = false, - includeTotalCount = true, - includeChannel = true, - includeChannelCustom = true, - includeChannelType = true, - includeChannelStatus = false - ) - ).then { setMembershipsResult -> - MembershipImpl.fromMembershipDTO(chat, setMembershipsResult.data.first(), user) - }.thenAsync { membership -> - chat.pubNub.time().thenAsync { time -> - membership.setLastReadMessageTimetoken(time.timetoken) - } - }.alsoAsync { - chat.emitEvent(user.id, EventContent.Invite(this.type ?: ChannelType.UNKNOWN, this.id)) - } - } - } - } - - override fun inviteMultiple(users: Collection): PNFuture> { - if (this.type == ChannelType.PUBLIC) { - return log.logErrorAndReturnException(CHANNEL_INVITES_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS).asFuture() - } - return chat.pubNub.setChannelMembers( - this.id, - users.map { PNMember.Partial(it.id) }, - include = MemberInclude( - includeCustom = true, - includeStatus = false, - includeType = false, - includeTotalCount = true, - includeUser = true, - includeUserCustom = true, - includeUserType = true, - includeUserStatus = false - ), - filter = users.joinToString(" || ") { it.uuidFilterString } - ).thenAsync { memberArrayResult: PNMemberArrayResult -> - chat.pubNub.time().thenAsync { time: PNTimeResult -> - val futures: List> = memberArrayResult.data.map { - MembershipImpl.fromChannelMemberDTO(chat, it, this).setLastReadMessageTimetoken(time.timetoken) - } - futures.awaitAll() - } - }.alsoAsync { - users.map { u -> - chat.emitEvent(u.id, EventContent.Invite(this.type ?: ChannelType.UNKNOWN, this.id)) - }.awaitAll() - } - } - - override fun getMembers( - limit: Int?, - page: PNPage?, - filter: String?, - sort: Collection>, - ): PNFuture { - return chat.pubNub.getChannelMembers( - channel = this.id, - limit = limit, - page = page, - filter = filter, - sort = sort, - include = MemberInclude( - includeCustom = true, - includeStatus = false, - includeType = false, - includeTotalCount = true, - includeUser = true, - includeUserCustom = true, - includeUserType = true, - includeUserStatus = false - ), - ).then { it: PNMemberArrayResult -> - MembersResponse( - it.next, - it.prev, - it.totalCount!!, - it.status, - it.data.map { - MembershipImpl.fromChannelMemberDTO(chat, it, this) - } - ) - } - } - - override fun connect(callback: (Message) -> Unit): AutoCloseable { + override fun connect(callback: (M) -> Unit): AutoCloseable { val channelEntity = chat.pubNub.channel(id) val subscription = channelEntity.subscription() val listener = createEventListener( @@ -489,7 +378,7 @@ abstract class BaseChannel( ) { return@createEventListener } - callback(MessageImpl.fromDTO(chat, pnMessageResult)) + callback(messageFactory2(chat, pnMessageResult)) } catch (e: Exception) { log.e(throwable = e) { ERROR_HANDLING_ONMESSAGE_EVENT } } @@ -500,45 +389,7 @@ abstract class BaseChannel( return subscription } - override fun join(custom: CustomObject?, callback: ((Message) -> Unit)?): PNFuture { - val user = this.chat.currentUser - return chat.pubNub.setMemberships( - channels = listOf( - PNChannelMembership.Partial( - this.id, - custom - ) - ), // todo should null overwrite? Waiting for optionals? - filter = channelFilterString, - include = MembershipInclude( - includeCustom = true, - includeStatus = false, - includeType = false, - includeTotalCount = true, - includeChannel = true, - includeChannelCustom = true, - includeChannelType = true, - includeChannelStatus = false - ) - ).thenAsync { membershipArray: PNChannelMembershipArrayResult -> - val resultDisconnect = callback?.let { connect(it) } - - chat.pubNub.time().thenAsync { time: PNTimeResult -> - MembershipImpl.fromMembershipDTO(chat, membershipArray.data.first(), user) - .setLastReadMessageTimetoken(time.timetoken) - }.then { membership: Membership -> - JoinResult( - membership, - resultDisconnect - ) - } - } - } - - // there is a discrepancy between KMP and JS. There is no unsubscribe here. This is agreed and will be changed in JS Chat - override fun leave(): PNFuture = chat.pubNub.removeMemberships(channels = listOf(id), include = MembershipInclude()).then { Unit } - - override fun getPinnedMessage(): PNFuture { + override fun getPinnedMessage(): PNFuture?> { val pinnedMessageTimetoken = this.custom?.get(PINNED_MESSAGE_TIMETOKEN).tryLong() ?: return null.asFuture() val pinnedMessageChannelID = this.custom?.get(PINNED_MESSAGE_CHANNEL_ID) as? String ?: return null.asFuture() @@ -553,15 +404,15 @@ abstract class BaseChannel( } } - override fun getMessage(timetoken: Long): PNFuture { - return getMessage(chat = chat, channelId = id, timetoken = timetoken) + override fun getMessage(timetoken: Long): PNFuture { + return getMessage(chat = chat, channelId = id, timetoken = timetoken, messageFactory) } override fun registerForPush() = chat.registerPushChannels(listOf(id)) override fun unregisterFromPush() = chat.unregisterPushChannels(listOf(id)) - override fun pinMessage(message: Message): PNFuture { + override fun pinMessage(message: BaseMessage<*, *>): PNFuture { return pinOrUnpinMessageToChannel(chat.pubNub, message, this).then { channelFactory(chat, it.data) } } @@ -626,35 +477,6 @@ abstract class BaseChannel( ) } - override fun streamReadReceipts(callback: (receipts: Map>) -> Unit): AutoCloseable { - if (type == ChannelType.PUBLIC) { - log.pnError(READ_RECEIPTS_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS) - } - val timetokensPerUser = mutableMapOf() - // in group chats it work till 100 members - val future = getMembers().then { members -> - members.members.forEach { m -> - val lastTimetoken = m.custom?.get(METADATA_LAST_READ_MESSAGE_TIMETOKEN)?.tryLong() - if (lastTimetoken != null) { - timetokensPerUser[m.user.id] = lastTimetoken - } - } - callback(generateReceipts(timetokensPerUser)) - }.then { - chat.listenForEvents(id) { event -> - timetokensPerUser[event.userId] = event.payload.messageTimetoken - callback(generateReceipts(timetokensPerUser)) - } - }.remember() - return AutoCloseable { - future.async { - it.onSuccess { subscription -> - subscription.close() - } - } - } - } - override fun getFiles(limit: Int, next: String?): PNFuture { return chat.pubNub.listFiles(id, limit, next?.let { PNPage.PNNext(it) }).thenAsync { listFilesResult -> val filesList = listFilesResult.data.toList() @@ -709,18 +531,6 @@ abstract class BaseChannel( } } - override fun getUserSuggestions(text: String, limit: Int): PNFuture> { - suggestedMemberships[text]?.let { nonNullMemberships -> - return nonNullMemberships.asFuture() - } - - return getMembers(filter = "uuid.name LIKE '$text*'", limit = limit).then { membersResponse -> - val memberships = membersResponse.members - suggestedMemberships[text] = memberships - memberships - } - } - override fun getMessageReportsHistory( startTimetoken: Long?, endTimetoken: Long?, @@ -778,7 +588,7 @@ abstract class BaseChannel( ) } - override operator fun plus(update: PNChannelMetadata): Channel { + override operator fun plus(update: PNChannelMetadata): C { return channelFactory(chat, toPNChannelMetadata() + update) } @@ -807,7 +617,7 @@ abstract class BaseChannel( companion object { private val log = Logger.withTag("BaseChannel") - fun getHistory( + fun , M : BaseMessage> getHistory( chat: ChatInternal, channelId: String, messageFactory: (ChatInternal, PNFetchMessageItem, channelId: String) -> M, @@ -836,12 +646,12 @@ abstract class BaseChannel( } } - fun getMessage(chat: ChatInternal, channelId: String, timetoken: Long): PNFuture { + fun , C : BaseChannel> getMessage(chat: ChatInternal, channelId: String, timetoken: Long, messageFactory: (ChatInternal, PNFetchMessageItem, String) -> M): PNFuture { val previousTimetoken = timetoken + 1 return getHistory( chat = chat, channelId = channelId, - messageFactory = MessageImpl::fromDTO, + messageFactory = messageFactory, startTimetoken = previousTimetoken, endTimetoken = timetoken, count = 1 @@ -850,9 +660,10 @@ abstract class BaseChannel( } } - fun streamUpdatesOn( - channels: Collection, - callback: (channels: Collection) -> Unit + fun , M : BaseMessage> streamUpdatesOn( + channels: Collection, + channelFactory: (ChatInternal, PNChannelMetadata) -> C, + callback: (channels: Collection) -> Unit, ): AutoCloseable { if (channels.isEmpty()) { log.pnError(CAN_NOT_STREAM_CHANNEL_UPDATES_ON_EMPTY_LIST) @@ -864,7 +675,7 @@ abstract class BaseChannel( is PNSetChannelMetadataEventMessage -> { val newChannelId = message.data.id val previousChannel = latestChannels.firstOrNull { it.id == newChannelId } - val newChannel = previousChannel?.plus(message.data) ?: ChannelImpl.fromDTO(chat, message.data) + val newChannel = previousChannel?.plus(message.data) ?: channelFactory(chat, message.data) newChannel to newChannelId } @@ -891,7 +702,7 @@ abstract class BaseChannel( } internal fun getPushPayload( - baseChannel: BaseChannel<*, *>, + baseChannel: BaseChannelImpl<*, *>, text: String, pushConfig: PushNotificationsConfig, customPushData: Map? = null diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ChannelImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ChannelImpl.kt index 4b8ec25d..83fab54d 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ChannelImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ChannelImpl.kt @@ -1,13 +1,47 @@ package com.pubnub.chat.internal.channel +import co.touchlab.kermit.Logger +import com.pubnub.api.models.consumer.PNTimeResult +import com.pubnub.api.models.consumer.objects.PNMemberKey +import com.pubnub.api.models.consumer.objects.PNPage +import com.pubnub.api.models.consumer.objects.PNSortKey import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata +import com.pubnub.api.models.consumer.objects.member.MemberInclude +import com.pubnub.api.models.consumer.objects.member.PNMember +import com.pubnub.api.models.consumer.objects.member.PNMemberArrayResult +import com.pubnub.api.models.consumer.objects.membership.MembershipInclude +import com.pubnub.api.models.consumer.objects.membership.PNChannelMembership +import com.pubnub.api.models.consumer.objects.membership.PNChannelMembershipArrayResult import com.pubnub.api.utils.Clock import com.pubnub.chat.Channel +import com.pubnub.chat.Membership import com.pubnub.chat.Message +import com.pubnub.chat.User import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.DELETED +import com.pubnub.chat.internal.METADATA_LAST_READ_MESSAGE_TIMETOKEN +import com.pubnub.chat.internal.MembershipImpl +import com.pubnub.chat.internal.error.PubNubErrorMessage.CHANNEL_INVITES_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS +import com.pubnub.chat.internal.error.PubNubErrorMessage.READ_RECEIPTS_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS import com.pubnub.chat.internal.message.MessageImpl +import com.pubnub.chat.internal.util.logErrorAndReturnException +import com.pubnub.chat.internal.util.pnError +import com.pubnub.chat.internal.uuidFilterString +import com.pubnub.chat.listenForEvents +import com.pubnub.chat.membership.MembersResponse import com.pubnub.chat.types.ChannelType +import com.pubnub.chat.types.EventContent +import com.pubnub.chat.types.JoinResult +import com.pubnub.kmp.CustomObject +import com.pubnub.kmp.PNFuture +import com.pubnub.kmp.alsoAsync +import com.pubnub.kmp.asFuture +import com.pubnub.kmp.awaitAll +import com.pubnub.kmp.remember +import com.pubnub.kmp.then +import com.pubnub.kmp.thenAsync +import tryLong +import kotlin.text.get data class ChannelImpl( override val chat: ChatInternal, @@ -19,7 +53,7 @@ data class ChannelImpl( override val updated: String? = null, override val status: String? = null, override val type: ChannelType? = null, -) : BaseChannel( +) : Channel, BaseChannelImpl( chat = chat, clock = clock, id = id, @@ -30,9 +64,12 @@ data class ChannelImpl( status = status, type = type, channelFactory = ::fromDTO, - messageFactory = MessageImpl::fromDTO + messageFactory = MessageImpl::fromDTO, + messageFactory2 = MessageImpl::fromDTO ) { companion object { + private val log = Logger.withTag("ChannelImpl") + fun fromDTO(chat: ChatInternal, channel: PNChannelMetadata): Channel { return ChannelImpl( chat, @@ -48,4 +85,189 @@ data class ChannelImpl( } override fun copyWithStatusDeleted(): Channel = copy(status = DELETED) + + override fun invite(user: User): PNFuture { + if (this.type == ChannelType.PUBLIC) { + return log.logErrorAndReturnException(CHANNEL_INVITES_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS).asFuture() + } + return getMembers(filter = user.uuidFilterString).thenAsync { channelMembers: MembersResponse -> + if (channelMembers.members.isNotEmpty()) { + return@thenAsync channelMembers.members.first().asFuture() + } else { + chat.pubNub.setMemberships( + channels = listOf(PNChannelMembership.Partial(this.id)), + userId = user.id, + filter = channelFilterString, + include = MembershipInclude( + includeCustom = true, + includeStatus = false, + includeType = false, + includeTotalCount = true, + includeChannel = true, + includeChannelCustom = true, + includeChannelType = true, + includeChannelStatus = false + ) + ).then { setMembershipsResult -> + MembershipImpl.fromMembershipDTO(chat, setMembershipsResult.data.first(), user) + }.thenAsync { membership -> + chat.pubNub.time().thenAsync { time -> + membership.setLastReadMessageTimetoken(time.timetoken) + } + }.alsoAsync { + chat.emitEvent(user.id, EventContent.Invite(this.type ?: ChannelType.UNKNOWN, this.id)) + } + } + } + } + + override fun inviteMultiple(users: Collection): PNFuture> { + if (this.type == ChannelType.PUBLIC) { + return log.logErrorAndReturnException(CHANNEL_INVITES_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS).asFuture() + } + return chat.pubNub.setChannelMembers( + this.id, + users.map { PNMember.Partial(it.id) }, + include = MemberInclude( + includeCustom = true, + includeStatus = false, + includeType = false, + includeTotalCount = true, + includeUser = true, + includeUserCustom = true, + includeUserType = true, + includeUserStatus = false + ), + filter = users.joinToString(" || ") { it.uuidFilterString } + ).thenAsync { memberArrayResult: PNMemberArrayResult -> + chat.pubNub.time().thenAsync { time: PNTimeResult -> + val futures: List> = memberArrayResult.data.map { + MembershipImpl.fromChannelMemberDTO(chat, it, this).setLastReadMessageTimetoken(time.timetoken) + } + futures.awaitAll() + } + }.alsoAsync { + users.map { u -> + chat.emitEvent(u.id, EventContent.Invite(this.type ?: ChannelType.UNKNOWN, this.id)) + }.awaitAll() + } + } + + override fun getMembers( + limit: Int?, + page: PNPage?, + filter: String?, + sort: Collection>, + ): PNFuture { + return chat.pubNub.getChannelMembers( + channel = this.id, + limit = limit, + page = page, + filter = filter, + sort = sort, + include = MemberInclude( + includeCustom = true, + includeStatus = false, + includeType = false, + includeTotalCount = true, + includeUser = true, + includeUserCustom = true, + includeUserType = true, + includeUserStatus = false + ), + ).then { it: PNMemberArrayResult -> + MembersResponse( + it.next, + it.prev, + it.totalCount!!, + it.status, + it.data.map { + MembershipImpl.fromChannelMemberDTO(chat, it, this) + } + ) + } + } + + override fun streamReadReceipts(callback: (receipts: Map>) -> Unit): AutoCloseable { + if (type == ChannelType.PUBLIC) { + log.pnError(READ_RECEIPTS_ARE_NOT_SUPPORTED_IN_PUBLIC_CHATS) + } + val timetokensPerUser = mutableMapOf() + // in group chats it work till 100 members + val future = getMembers().then { members -> + members.members.forEach { m -> + val lastTimetoken = m.custom?.get(METADATA_LAST_READ_MESSAGE_TIMETOKEN)?.tryLong() + if (lastTimetoken != null) { + timetokensPerUser[m.user.id] = lastTimetoken + } + } + callback(generateReceipts(timetokensPerUser)) + }.then { + chat.listenForEvents(id) { event -> + timetokensPerUser[event.userId] = event.payload.messageTimetoken + callback(generateReceipts(timetokensPerUser)) + } + }.remember() + return AutoCloseable { + future.async { + it.onSuccess { subscription -> + subscription.close() + } + } + } + } + + private val suggestedMemberships = mutableMapOf>() + + override fun getUserSuggestions(text: String, limit: Int): PNFuture> { + suggestedMemberships[text]?.let { nonNullMemberships -> + return nonNullMemberships.asFuture() + } + + return getMembers(filter = "uuid.name LIKE '$text*'", limit = limit).then { membersResponse -> + val memberships = membersResponse.members + suggestedMemberships[text] = memberships + memberships + } + } + + override fun join(custom: CustomObject?, callback: ((Message) -> Unit)?): PNFuture { + val user = this.chat.currentUser + return chat.pubNub.setMemberships( + channels = listOf( + PNChannelMembership.Partial( + this.id, + custom + ) + ), // todo should null overwrite? Waiting for optionals? + filter = channelFilterString, + include = MembershipInclude( + includeCustom = true, + includeStatus = false, + includeType = false, + includeTotalCount = true, + includeChannel = true, + includeChannelCustom = true, + includeChannelType = true, + includeChannelStatus = false + ) + ).thenAsync { membershipArray: PNChannelMembershipArrayResult -> + val resultDisconnect = callback?.let { connect(it) } + + chat.pubNub.time().thenAsync { time: PNTimeResult -> + MembershipImpl.fromMembershipDTO(chat, membershipArray.data.first(), user) + .setLastReadMessageTimetoken(time.timetoken) + }.then { membership: Membership -> + JoinResult( + membership, + resultDisconnect + ) + } + } + } + + // there is a discrepancy between KMP and JS. There is no unsubscribe here. This is agreed and will be changed in JS Chat + override fun leave(): PNFuture = chat.pubNub.removeMemberships(channels = listOf(id), include = MembershipInclude()).then { Unit } + + private val channelFilterString get() = "channel.id == '${this.id}'" } diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ThreadChannelImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ThreadChannelImpl.kt index 7eb2fd06..1c0c0791 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ThreadChannelImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/channel/ThreadChannelImpl.kt @@ -5,7 +5,9 @@ import com.pubnub.api.models.consumer.PNPublishResult import com.pubnub.api.models.consumer.message_actions.PNMessageAction import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadata import com.pubnub.api.utils.Clock +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel +import com.pubnub.chat.Membership import com.pubnub.chat.Message import com.pubnub.chat.ThreadChannel import com.pubnub.chat.ThreadMessage @@ -40,7 +42,8 @@ data class ThreadChannelImpl( override val status: String? = null, override val type: ChannelType? = null, private var threadCreated: Boolean = true, -) : BaseChannel( + private var parentChannel: Channel? = null +) : BaseChannelImpl( chat, clock, id, @@ -55,12 +58,37 @@ data class ThreadChannelImpl( }, { chat, pnMessageItem, channelId -> ThreadMessageImpl.fromDTO(chat, pnMessageItem, channelId, parentMessage.channelId) - } + }, + { chat, messageResult -> + ThreadMessageImpl.fromDTO(chat, messageResult, parentMessage.channelId) + }, ), ThreadChannel { override val parentChannelId: String get() = parentMessage.channelId + private val suggestedMemberships = mutableMapOf>() + + override fun getUserSuggestions(text: String, limit: Int): PNFuture> { + suggestedMemberships[text]?.let { nonNullMemberships -> + return nonNullMemberships.asFuture() + } + + return ( + parentChannel?.asFuture() + ?: chat.getChannel(parentChannelId).then { channel -> + parentChannel = channel + channel + } + ).thenAsync { + parentChannel?.getMembers(filter = "uuid.name LIKE '$text*'", limit = limit)?.then { membersResponse -> + val memberships = membersResponse.members + suggestedMemberships[text] = memberships + memberships + } ?: emptyList().asFuture() + } + } + override fun pinMessageToParentChannel(message: ThreadMessage) = pinOrUnpinMessageFromParentChannel(message) override fun unpinMessageFromParentChannel(): PNFuture = pinOrUnpinMessageFromParentChannel(null) @@ -76,7 +104,7 @@ data class ThreadChannelImpl( } } - override fun delete(soft: Boolean): PNFuture { + override fun delete(soft: Boolean): PNFuture { return chat.removeThreadChannel(chat, parentMessage, soft).then { it.second } } @@ -113,7 +141,7 @@ data class ThreadChannelImpl( mentionedUsers: MessageMentionedUsers?, referencedChannels: MessageReferencedChannels?, textLinks: List?, - quotedMessage: Message?, + quotedMessage: BaseMessage<*, *>?, files: List?, customPushData: Map?, ): PNFuture { @@ -140,7 +168,7 @@ data class ThreadChannelImpl( shouldStore: Boolean, usePost: Boolean, ttl: Int?, - quotedMessage: Message?, + quotedMessage: BaseMessage<*, *>?, files: List?, usersToMention: Collection?, customPushData: Map?, diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/BaseMessage.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/BaseMessageImpl.kt similarity index 86% rename from pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/BaseMessage.kt rename to pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/BaseMessageImpl.kt index 8b055db3..46e08ffd 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/BaseMessage.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/BaseMessageImpl.kt @@ -10,10 +10,9 @@ import com.pubnub.api.models.consumer.PNPublishResult import com.pubnub.api.models.consumer.history.PNFetchMessageItem import com.pubnub.api.models.consumer.message_actions.PNAddMessageActionResult import com.pubnub.api.models.consumer.message_actions.PNMessageAction -import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult -import com.pubnub.chat.Channel -import com.pubnub.chat.Message -import com.pubnub.chat.ThreadChannel +import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadataResult +import com.pubnub.chat.BaseChannel +import com.pubnub.chat.BaseMessage import com.pubnub.chat.internal.ChatImpl import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.INTERNAL_MODERATION_PREFIX @@ -23,7 +22,6 @@ import com.pubnub.chat.internal.METADATA_REFERENCED_CHANNELS import com.pubnub.chat.internal.METADATA_TEXT_LINKS import com.pubnub.chat.internal.PUBNUB_INTERNAL_AUTOMODERATED import com.pubnub.chat.internal.THREAD_ROOT_ID -import com.pubnub.chat.internal.channel.ChannelImpl import com.pubnub.chat.internal.error.PubNubErrorMessage import com.pubnub.chat.internal.error.PubNubErrorMessage.AUTOMODERATED_MESSAGE_CANNOT_BE_EDITED import com.pubnub.chat.internal.error.PubNubErrorMessage.CANNOT_STREAM_MESSAGE_UPDATES_ON_EMPTY_LIST @@ -53,7 +51,7 @@ import tryInt typealias Actions = Map>> -abstract class BaseMessage( +abstract class BaseMessageImpl, C : BaseChannel>( override val chat: ChatInternal, override val timetoken: Long, override val content: EventContent.TextMessageContent, @@ -62,7 +60,7 @@ abstract class BaseMessage( override val actions: Map>>? = null, private val metaInternal: JsonElement? = null, override val error: PubNubError? = null, -) : Message { +) : BaseMessage { override val meta: Map? get() = metaInternal?.decode() as? Map override val quotedMessage: QuotedMessage? get() = metaInternal.extractQuotedMessage() override val mentionedUsers: MessageMentionedUsers? get() = metaInternal.extractMentionedUsers() @@ -85,11 +83,6 @@ abstract class BaseMessage( override val deleted: Boolean get() = getDeleteActions() != null - override val hasThread: Boolean - get() { - return actions?.get(THREAD_ROOT_ID)?.values?.firstOrNull()?.isNotEmpty() ?: false - } - @OptIn(ExperimentalSerializationApi::class) override val type = EventContent.TextMessageContent.serializer().descriptor.serialName // = "text" @@ -117,7 +110,7 @@ abstract class BaseMessage( return reactions[reaction]?.any { it.uuid == chat.pubNub.configuration.userId.value } ?: false } - override fun editText(newText: String): PNFuture { + override fun editText(newText: String): PNFuture { val type = chat.editMessageActionName if (this.meta?.containsKey(PUBNUB_INTERNAL_AUTOMODERATED) == true && !this.chat.currentUser.isInternalModerator) { return log.logErrorAndReturnException(AUTOMODERATED_MESSAGE_CANNOT_BE_EDITED).asFuture() @@ -136,7 +129,7 @@ abstract class BaseMessage( } } - override fun delete(soft: Boolean, preserveFiles: Boolean): PNFuture { + override fun delete(soft: Boolean, preserveFiles: Boolean): PNFuture { val type = chat.deleteMessageActionName if (soft) { var updatedActions: Actions = actions ?: mapOf() @@ -179,18 +172,14 @@ abstract class BaseMessage( } } - override fun getThread() = chat.getThreadChannel(this) - override fun forward(channelId: String): PNFuture = chat.forwardMessage(this, channelId) - override fun pin(): PNFuture { + protected fun pinInternal(): PNFuture { return chat.getChannel(channelId).thenAsync { channel -> if (channel == null) { log.pnError(PubNubErrorMessage.CHANNEL_NOT_EXIST) } - ChatImpl.pinOrUnpinMessageToChannel(chat.pubNub, this, channel).then { - ChannelImpl.fromDTO(chat, it.data) - } + ChatImpl.pinOrUnpinMessageToChannel(chat.pubNub, this, channel) } } @@ -208,11 +197,7 @@ abstract class BaseMessage( ) } - override fun createThread(): PNFuture = ChatImpl.createThreadChannel(chat, this) - - override fun removeThread(): PNFuture> = chat.removeThreadChannel(chat, this) - - override fun toggleReaction(reaction: String): PNFuture { + override fun toggleReaction(reaction: String): PNFuture { val existingReaction = reactions[reaction]?.find { it.uuid == chat.currentUser.id } @@ -231,29 +216,22 @@ abstract class BaseMessage( return newActions.then { copyWithActions(it) } } - override fun streamUpdates(callback: (message: M) -> Unit): AutoCloseable { - return streamUpdatesOn(listOf(this as M)) { + override fun streamUpdates(callback: (message: M) -> Unit): AutoCloseable { + return streamUpdatesOn(listOf(this as M)) { callback(it.first()) } } - override fun restore(): PNFuture { + open override fun restore(): PNFuture { val deleteActions: List = getDeleteActions() - ?: return this.also { log.w(THIS_MESSAGE_HAS_NOT_BEEN_DELETED) }.asFuture() + ?: return (this as M).also { log.w(THIS_MESSAGE_HAS_NOT_BEEN_DELETED) }.asFuture() var updatedActions: Actions? = actions?.filterNot { it.key == chat.deleteMessageActionName } return deleteActions .map { removeMessageAction(it.actionTimetoken) } .awaitAll() - .thenAsync { - // attempt to restore the thread channel related to this message if exists - chat.restoreThreadChannel(this) - }.then { addThreadRootIdMessageAction: PNMessageAction? -> - // update actions map by adding THREAD_ROOT_ID if there is thread related to the message - addThreadRootIdMessageAction?.let { notNullAction -> - updatedActions = assignAction(updatedActions, notNullAction) - } + .then { copyWithActions(updatedActions) } } @@ -270,12 +248,7 @@ abstract class BaseMessage( return actions?.get(chat.deleteMessageActionName)?.get(chat.deleteMessageActionName) } - private fun deleteThread(soft: Boolean): PNFuture { - if (hasThread) { - return getThread().thenAsync { - it.delete(soft) - }.then { Unit } - } + protected open fun deleteThread(soft: Boolean): PNFuture { return Unit.asFuture() } @@ -287,16 +260,16 @@ abstract class BaseMessage( ) } - internal abstract fun copyWithActions(actions: Actions?): T + internal abstract fun copyWithActions(actions: Actions?): M - internal abstract fun copyWithContent(content: EventContent.TextMessageContent): T + internal abstract fun copyWithContent(content: EventContent.TextMessageContent): M companion object { private val log = Logger.withTag("BaseMessageImpl") - fun streamUpdatesOn( - messages: Collection, - callback: (messages: Collection) -> Unit, + fun , C : BaseChannel> streamUpdatesOn( + messages: Collection, + callback: (messages: Collection) -> Unit, ): AutoCloseable { if (messages.isEmpty()) { log.pnError(CANNOT_STREAM_MESSAGE_UPDATES_ON_EMPTY_LIST) @@ -321,7 +294,7 @@ abstract class BaseMessage( event.messageAction ) } - val newMessage = (message as BaseMessage).copyWithActions(actions) + val newMessage = (message as BaseMessageImpl).copyWithActions(actions) latestMessages = latestMessages.map { if (it.timetoken == newMessage.timetoken) { newMessage diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt index 0c176e83..c2e578de 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt @@ -1,14 +1,32 @@ package com.pubnub.chat.internal.message +import co.touchlab.kermit.Logger import com.pubnub.api.JsonElement import com.pubnub.api.PubNubError import com.pubnub.api.models.consumer.history.PNFetchMessageItem import com.pubnub.api.models.consumer.history.PNFetchMessageItem.Action +import com.pubnub.api.models.consumer.message_actions.PNMessageAction +import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult import com.pubnub.api.models.consumer.pubsub.PNMessageResult +import com.pubnub.chat.Channel import com.pubnub.chat.Message +import com.pubnub.chat.ThreadChannel +import com.pubnub.chat.internal.ChatImpl.Companion.getThreadId import com.pubnub.chat.internal.ChatInternal +import com.pubnub.chat.internal.MESSAGE_THREAD_ID_PREFIX +import com.pubnub.chat.internal.THREAD_ROOT_ID +import com.pubnub.chat.internal.channel.ChannelImpl +import com.pubnub.chat.internal.channel.ThreadChannelImpl import com.pubnub.chat.internal.defaultGetMessageResponseBody +import com.pubnub.chat.internal.error.PubNubErrorMessage.ONLY_ONE_LEVEL_OF_THREAD_NESTING_IS_ALLOWED +import com.pubnub.chat.internal.error.PubNubErrorMessage.THREAD_FOR_THIS_MESSAGE_ALREADY_EXISTS +import com.pubnub.chat.internal.error.PubNubErrorMessage.YOU_CAN_NOT_CREATE_THREAD_ON_DELETED_MESSAGES +import com.pubnub.chat.internal.util.logErrorAndReturnException import com.pubnub.chat.types.EventContent +import com.pubnub.kmp.PNFuture +import com.pubnub.kmp.asFuture +import com.pubnub.kmp.then +import com.pubnub.kmp.thenAsync data class MessageImpl( override val chat: ChatInternal, @@ -19,7 +37,7 @@ data class MessageImpl( override val actions: Map>>? = null, val metaInternal: JsonElement? = null, override val error: PubNubError? = null, -) : BaseMessage( +) : Message, BaseMessageImpl( chat = chat, timetoken = timetoken, content = content, @@ -27,13 +45,76 @@ data class MessageImpl( userId = userId, actions = actions, metaInternal = metaInternal, - error = error + error = error, ) { + override val hasThread: Boolean + get() { + return actions?.get(THREAD_ROOT_ID)?.values?.firstOrNull()?.isNotEmpty() ?: false + } + + override fun getThread() = chat.getThreadChannel(this) + + override fun createThread(): PNFuture { + if (channelId.startsWith(MESSAGE_THREAD_ID_PREFIX)) { + return log.logErrorAndReturnException(ONLY_ONE_LEVEL_OF_THREAD_NESTING_IS_ALLOWED).asFuture() + } + if (deleted) { + return log.logErrorAndReturnException(YOU_CAN_NOT_CREATE_THREAD_ON_DELETED_MESSAGES).asFuture() + } + + val threadChannelId = getThreadId(channelId, timetoken) + return chat.getChannel(threadChannelId).thenAsync { it: Channel? -> + if (it != null) { + return@thenAsync log.logErrorAndReturnException(THREAD_FOR_THIS_MESSAGE_ALREADY_EXISTS) + .asFuture() + } + ThreadChannelImpl( + this, + chat, + description = "Thread on channel $channelId with message timetoken $timetoken", + id = threadChannelId, + threadCreated = false + ).asFuture() + } + } + + override fun removeThread(): PNFuture> = chat.removeThreadChannel(chat, this) + + override fun deleteThread(soft: Boolean): PNFuture { + if (hasThread) { + return getThread().thenAsync { + it.delete(soft) + }.then { Unit } + } + return Unit.asFuture() + } + override fun copyWithActions(actions: Actions?): Message = copy(actions = actions) override fun copyWithContent(content: EventContent.TextMessageContent): Message = copy(content = content) + override fun restore(): PNFuture { + return super.restore().thenAsync { message -> + // attempt to restore the thread channel related to this message if exists + chat.restoreThreadChannel(this).then { addThreadRootIdMessageAction: PNMessageAction? -> + // update actions map by adding THREAD_ROOT_ID if there is thread related to the message + addThreadRootIdMessageAction?.let { notNullAction -> + val updatedActions = assignAction(message.actions, notNullAction) + (message as MessageImpl).copyWithActions(updatedActions) + } ?: message + } + } + } + + override fun pin(): PNFuture { + return pinInternal().then { + ChannelImpl.fromDTO(chat, it.data) + } + } + companion object { + private val log = Logger.withTag("MessageImpl") + internal fun fromDTO(chat: ChatInternal, pnMessageResult: PNMessageResult): Message { val content = chat.config.customPayloads?.getMessageResponseBody?.invoke(pnMessageResult.message, pnMessageResult.channel, ::defaultGetMessageResponseBody) diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt index 2a4a3486..3afada58 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt @@ -7,10 +7,13 @@ import com.pubnub.api.models.consumer.history.PNFetchMessageItem import com.pubnub.api.models.consumer.history.PNFetchMessageItem.Action import com.pubnub.api.models.consumer.pubsub.PNMessageResult import com.pubnub.chat.Channel +import com.pubnub.chat.ThreadChannel import com.pubnub.chat.ThreadMessage import com.pubnub.chat.internal.ChatImpl +import com.pubnub.chat.internal.ChatImpl.Companion.getParentMessageTimetokenFromThreadId import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.channel.ChannelImpl +import com.pubnub.chat.internal.channel.ThreadChannelImpl import com.pubnub.chat.internal.defaultGetMessageResponseBody import com.pubnub.chat.internal.error.PubNubErrorMessage.PARENT_CHANNEL_DOES_NOT_EXISTS import com.pubnub.chat.internal.util.pnError @@ -29,7 +32,7 @@ data class ThreadMessageImpl( override val actions: Map>>? = null, val metaInternal: JsonElement? = null, override val error: PubNubError? = null, -) : BaseMessage( +) : BaseMessageImpl( chat = chat, timetoken = timetoken, content = content, @@ -37,9 +40,11 @@ data class ThreadMessageImpl( userId = userId, actions = actions, metaInternal = metaInternal, - error = error + error = error, ), ThreadMessage { + val parentMessageTimetoken get() = getParentMessageTimetokenFromThreadId(channelId) + override fun copyWithActions(actions: Actions?): ThreadMessage = copy(actions = actions) override fun copyWithContent(content: EventContent.TextMessageContent): ThreadMessage = copy(content = content) @@ -47,7 +52,7 @@ data class ThreadMessageImpl( companion object { private val log = Logger.withTag("ThreadMessageImpl") - internal fun fromDTO(chat: ChatImpl, pnMessageResult: PNMessageResult, parentChannelId: String): ThreadMessage { + internal fun fromDTO(chat: ChatInternal, pnMessageResult: PNMessageResult, parentChannelId: String): ThreadMessage { val content = chat.config.customPayloads?.getMessageResponseBody?.invoke(pnMessageResult.message, pnMessageResult.channel, ::defaultGetMessageResponseBody) ?: defaultGetMessageResponseBody(pnMessageResult.message) @@ -60,7 +65,7 @@ data class ThreadMessageImpl( pnMessageResult.channel, pnMessageResult.publisher!!, metaInternal = pnMessageResult.userMetadata, - error = pnMessageResult.error + error = pnMessageResult.error, ) } @@ -79,7 +84,7 @@ data class ThreadMessageImpl( userId = messageItem.uuid!!, actions = messageItem.actions, metaInternal = messageItem.meta, - error = messageItem.error + error = messageItem.error, ) } } @@ -98,4 +103,12 @@ data class ThreadMessageImpl( } } } + + override fun pin(): PNFuture { + return pinInternal().thenAsync { parentChannelMetadata -> + ChannelImpl(chat, id = parentChannelId).getMessage(parentMessageTimetoken).then { parentMessage -> + ThreadChannelImpl.fromDTO(chat, requireNotNull(parentMessage), parentChannelMetadata.data) + } + } + } } diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChannelIntegrationTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChannelIntegrationTest.kt index 3e6d358a..4971487d 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChannelIntegrationTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChannelIntegrationTest.kt @@ -13,7 +13,7 @@ import com.pubnub.chat.internal.INTERNAL_MODERATION_PREFIX import com.pubnub.chat.internal.MessageDraftImpl import com.pubnub.chat.internal.PINNED_MESSAGE_TIMETOKEN import com.pubnub.chat.internal.UserImpl -import com.pubnub.chat.internal.channel.BaseChannel +import com.pubnub.chat.internal.channel.BaseChannelImpl import com.pubnub.chat.internal.channel.ChannelImpl import com.pubnub.chat.restrictions.GetRestrictionsResponse import com.pubnub.chat.types.EventContent @@ -466,7 +466,7 @@ class ChannelIntegrationTest : BaseChatIntegrationTest() { pubnub.test(backgroundScope, checkAllEvents = false) { var dispose: AutoCloseable? = null pubnub.awaitSubscribe(listOf(channel01.id, channel02.id)) { - dispose = BaseChannel.streamUpdatesOn(listOf(channel01, channel02)) { channels -> + dispose = BaseChannelImpl.streamUpdatesOn(listOf(channel01, channel02), ChannelImpl::fromDTO) { channels -> actualUpdates.add(channels.map { it.asImpl().copy(updated = null) }.sortedBy { it.id }) } } diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/MessageIntegrationTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/MessageIntegrationTest.kt index bec5efd2..fb950de6 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/MessageIntegrationTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/MessageIntegrationTest.kt @@ -7,7 +7,7 @@ import com.pubnub.chat.ThreadChannel import com.pubnub.chat.ThreadMessage import com.pubnub.chat.internal.INTERNAL_MODERATION_PREFIX import com.pubnub.chat.internal.MESSAGE_THREAD_ID_PREFIX -import com.pubnub.chat.internal.message.BaseMessage +import com.pubnub.chat.internal.message.BaseMessageImpl import com.pubnub.chat.internal.message.MessageImpl import com.pubnub.chat.listenForEvents import com.pubnub.chat.types.EventContent @@ -176,7 +176,7 @@ class MessageIntegrationTest : BaseChatIntegrationTest() { pubnub.test(backgroundScope, checkAllEvents = false) { var dispose: AutoCloseable? = null pubnub.awaitSubscribe(listOf(channel01.id)) { - dispose = BaseMessage.streamUpdatesOn(listOf(message1, message2)) { messages -> + dispose = BaseMessageImpl.streamUpdatesOn(listOf(message1, message2)) { messages -> actualUpdates.add(messages.sortedBy { it.timetoken }) } } diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChannelTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChannelTest.kt index 70d0acc4..6e39b94b 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChannelTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChannelTest.kt @@ -37,7 +37,7 @@ import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.INTERNAL_MODERATION_PREFIX import com.pubnub.chat.internal.MINIMAL_TYPING_INDICATOR_TIMEOUT import com.pubnub.chat.internal.UserImpl -import com.pubnub.chat.internal.channel.BaseChannel +import com.pubnub.chat.internal.channel.BaseChannelImpl import com.pubnub.chat.internal.channel.ChannelImpl import com.pubnub.chat.internal.message.MessageImpl import com.pubnub.chat.internal.mutelist.MutedUsersManagerImpl @@ -126,7 +126,10 @@ class ChannelTest : BaseTest() { @Test fun canUpdateChannel() { - every { chat.updateChannel(any(), any(), any(), any(), any(), any()) } returns objectUnderTest.asFuture() + every { chat.setChannelMetadata(any(), any(), any(), any(), any(), any()) } returns PNChannelMetadataResult( + 200, + PNChannelMetadata("a") + ).asFuture() objectUnderTest.update( name = name, @@ -136,19 +139,18 @@ class ChannelTest : BaseTest() { type = type, ).async {} - verify { chat.updateChannel(channelId, name, custom, description, status, type) } + verify { chat.setChannelMetadata(channelId, name, description, custom, type, status) } } @Test fun canSoftDeleteChannel() { val softDelete = true - val channelFutureMock: PNFuture = mock(MockMode.strict) - every { chat.deleteChannel(any(), any()) } returns channelFutureMock + val channelFutureMock: PNFuture = mock(MockMode.strict) + every { chat.performDeleteChannel(any(), any()) } returns channelFutureMock - val deleteChannelFuture: PNFuture = objectUnderTest.delete(soft = softDelete) + objectUnderTest.delete(soft = softDelete) - assertEquals(channelFutureMock, deleteChannelFuture) - verify { chat.deleteChannel(id = channelId, soft = softDelete) } + verify { chat.performDeleteChannel(id = channelId, soft = softDelete) } } @Test @@ -337,7 +339,7 @@ class ChannelTest : BaseTest() { val user2 = "user2" typingIndicatorsForTest[user1] = typingSent1 typingIndicatorsForTest[user2] = typingSent1.plus(2.milliseconds) - BaseChannel.removeExpiredTypingIndicators( + BaseChannelImpl.removeExpiredTypingIndicators( objectUnderTest.chat.config.typingTimeout, typingIndicatorsForTest, now @@ -357,7 +359,7 @@ class ChannelTest : BaseTest() { typingIndicatorsForTest[user1] = typingSent1 typingIndicatorsForTest[user2] = typingSent1.plus(2.milliseconds) - BaseChannel.removeExpiredTypingIndicators( + BaseChannelImpl.removeExpiredTypingIndicators( objectUnderTest.chat.config.typingTimeout, typingIndicatorsForTest, now @@ -374,7 +376,7 @@ class ChannelTest : BaseTest() { val isTyping = true val typingIndicators = mutableMapOf() - BaseChannel.updateUserTypingStatus(userId, isTyping, now, typingIndicators) + BaseChannelImpl.updateUserTypingStatus(userId, isTyping, now, typingIndicators) assertTrue(typingIndicators.contains(userId)) } @@ -387,7 +389,7 @@ class ChannelTest : BaseTest() { val isTyping = false val typingIndicators = mutableMapOf(userId to typingSent1) - BaseChannel.updateUserTypingStatus(userId, isTyping, now, typingIndicators) + BaseChannelImpl.updateUserTypingStatus(userId, isTyping, now, typingIndicators) assertFalse(typingIndicators.contains(userId)) } @@ -400,7 +402,7 @@ class ChannelTest : BaseTest() { val isTyping = true val typingIndicators = mutableMapOf(userId to typingSent1) - BaseChannel.updateUserTypingStatus(userId, isTyping, now, typingIndicators) + BaseChannelImpl.updateUserTypingStatus(userId, isTyping, now, typingIndicators) assertTrue(typingIndicators.contains(userId)) assertEquals(now, typingIndicators[userId]) @@ -961,7 +963,7 @@ class ChannelTest : BaseTest() { fun getPushPayload_empty_when_sendPushes_is_false() { val config = PushNotificationsConfig(false, null, PNPushType.FCM, null, PNPushEnvironment.PRODUCTION) - val result = BaseChannel.getPushPayload(createChannel(type), "some text", config) + val result = BaseChannelImpl.getPushPayload(createChannel(type), "some text", config) assertEquals(emptyMap(), result) } @@ -974,7 +976,7 @@ class ChannelTest : BaseTest() { every { chat.currentUser } returns UserImpl(chat, userId) - val result = BaseChannel.getPushPayload(createChannel(type), text, config) + val result = BaseChannelImpl.getPushPayload(createChannel(type), text, config) assertEquals(objectUnderTest.name, result["pn_fcm"]["data"]["subtitle"]) assertEquals(userId, result["pn_fcm"]["notification"]["title"]) @@ -992,7 +994,7 @@ class ChannelTest : BaseTest() { val config = PushNotificationsConfig(true, "abc", PNPushType.FCM, topic, PNPushEnvironment.PRODUCTION) every { chat.currentUser } returns UserImpl(chat, userId) - val result = BaseChannel.getPushPayload(createChannel(type), text, config) + val result = BaseChannelImpl.getPushPayload(createChannel(type), text, config) assertEquals(objectUnderTest.name, result["pn_fcm"]["data"]["subtitle"]) @@ -1016,7 +1018,7 @@ class ChannelTest : BaseTest() { @Test fun generateReceipts() { - val result = BaseChannel.generateReceipts(mapOf("user" to 1L, "user2" to 2L, "user3" to 1L, "user4" to 3L)) + val result = BaseChannelImpl.generateReceipts(mapOf("user" to 1L, "user2" to 2L, "user3" to 1L, "user4" to 3L)) assertEquals(mapOf(1L to listOf("user", "user3"), 2L to listOf("user2"), 3L to listOf("user4")), result) } @@ -1039,11 +1041,11 @@ class ChannelTest : BaseTest() { @Test fun update_calls_chat() { - every { chat.updateChannel(any(), any(), any(), any(), any(), any()) } returns mock() + every { chat.setChannelMetadata(any(), any(), any(), any(), any(), any()) } returns mock() objectUnderTest.update(name, custom, description, status, type) - verify { chat.updateChannel(channelId, name, custom, description, status, type) } + verify { chat.setChannelMetadata(channelId, name, description, custom, type, status) } } @Test diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChatTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChatTest.kt index e9dd8a97..d543c446 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChatTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/ChatTest.kt @@ -55,8 +55,12 @@ import com.pubnub.chat.User import com.pubnub.chat.config.ChatConfiguration import com.pubnub.chat.config.PushNotificationsConfig import com.pubnub.chat.internal.ChatImpl +import com.pubnub.chat.internal.ChatImpl.Companion.getParentChannelIdFromThreadId +import com.pubnub.chat.internal.ChatImpl.Companion.getParentMessageTimetokenFromThreadId +import com.pubnub.chat.internal.ChatImpl.Companion.getThreadId import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.INTERNAL_USER_MODERATION_CHANNEL_PREFIX +import com.pubnub.chat.internal.MESSAGE_THREAD_ID_PREFIX import com.pubnub.chat.internal.message.MessageImpl import com.pubnub.chat.internal.timer.TimerManager import com.pubnub.chat.message.GetUnreadMessagesCounts @@ -1558,6 +1562,21 @@ class ChatTest : BaseTest() { ) } + @Test + fun threadIdEncodingDecoding() { + val channelId = "some_channel" + val messageTimetoken = 123456789L + + val threadId = getThreadId(channelId, messageTimetoken) + assertEquals("${MESSAGE_THREAD_ID_PREFIX}_${channelId}_$messageTimetoken", threadId) + + val decodedChannelId = getParentChannelIdFromThreadId(threadId) + assertEquals(channelId, decodedChannelId) + + val decodedMessageTimetoken = getParentMessageTimetokenFromThreadId(threadId) + assertEquals(messageTimetoken, decodedMessageTimetoken) + } + @Test fun destroy() { every { timerManager.destroy() } returns Unit diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt index c4298020..f52ed28d 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt @@ -8,8 +8,10 @@ import com.pubnub.api.models.consumer.objects.PNKey import com.pubnub.api.models.consumer.objects.PNMembershipKey import com.pubnub.api.models.consumer.objects.PNPage import com.pubnub.api.models.consumer.objects.PNSortKey +import com.pubnub.api.models.consumer.objects.channel.PNChannelMetadataResult import com.pubnub.api.models.consumer.push.PNPushAddChannelResult import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult +import com.pubnub.chat.BaseMessage import com.pubnub.chat.Channel import com.pubnub.chat.Chat import com.pubnub.chat.Event @@ -46,6 +48,36 @@ abstract class FakeChat(override val config: ChatConfiguration, override val pub TODO("Not yet implemented") } + override fun forwardMessage(message: BaseMessage<*, *>, channelId: String): PNFuture { + TODO("Not yet implemented") + } + + override fun setChannelMetadata( + id: String, + name: String?, + description: String?, + custom: CustomObject?, + type: ChannelType?, + status: String? + ): PNFuture { + TODO("Not yet implemented") + } + + override fun performCreateChannel( + id: String, + name: String?, + description: String?, + custom: CustomObject?, + type: ChannelType?, + status: String? + ): PNFuture { + TODO("Not yet implemented") + } + + override fun performDeleteChannel(id: String, soft: Boolean): PNFuture { + TODO("Not yet implemented") + } + override val reactionsActionName: String get() = TODO("Not yet implemented") @@ -65,7 +97,7 @@ abstract class FakeChat(override val config: ChatConfiguration, override val pub chat: Chat, message: Message, soft: Boolean - ): PNFuture> { + ): PNFuture> { TODO("Not yet implemented") } @@ -176,10 +208,6 @@ abstract class FakeChat(override val config: ChatConfiguration, override val pub TODO("Not yet implemented") } - override fun forwardMessage(message: Message, channelId: String): PNFuture { - TODO("Not yet implemented") - } - override fun whoIsPresent(channelId: String): PNFuture> { TODO("Not yet implemented") } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/BaseChannelJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/BaseChannelJs.kt new file mode 100644 index 00000000..0052d424 --- /dev/null +++ b/pubnub-chat-impl/src/jsMain/kotlin/BaseChannelJs.kt @@ -0,0 +1,284 @@ +@file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) + +import com.pubnub.chat.BaseChannel +import com.pubnub.chat.Channel +import com.pubnub.chat.MessageDraft +import com.pubnub.chat.ThreadChannel +import com.pubnub.chat.internal.MessageDraftImpl +import com.pubnub.chat.types.ChannelType +import com.pubnub.chat.types.InputFile +import com.pubnub.kmp.JsMap +import com.pubnub.kmp.UploadableImpl +import com.pubnub.kmp.asFuture +import com.pubnub.kmp.createJsObject +import com.pubnub.kmp.then +import com.pubnub.kmp.toJsMap +import com.pubnub.kmp.toMap +import kotlin.js.Json +import kotlin.js.Promise +import kotlin.js.json + +@JsExport +@JsName("BaseChannel") +open class BaseChannelJs internal constructor(internal val baseChannel: BaseChannel<*, *>, internal val chatJs: ChatJs) : ChannelFields { + override val id: String by baseChannel::id + override val name: String? by baseChannel::name + override val custom: Any? get() = baseChannel.custom?.toJsMap() + override val description: String? by baseChannel::description + override val updated: String? by baseChannel::updated + override val status: String? by baseChannel::status + override val type: String? get() = baseChannel.type?.stringValue + + fun update(data: ChannelFields): Promise { + return baseChannel.update( + data.name, + data.custom?.let { convertToCustomObject(it) }, + data.description, + data.status, + data.type?.let { ChannelType.from(it) } + ).then { + it.asJs(chatJs) + }.asPromise() + } + + fun delete(options: DeleteParameters?): Promise { + return baseChannel.delete(options?.soft ?: false).then { + if (it != null) { + DeleteChannelResult(it.asJs(chatJs)) + } else { + DeleteChannelResult(true) + } + }.asPromise() + } + + fun streamUpdates(callback: (channel: BaseChannelJs?) -> Unit): () -> Unit { + val closeable = baseChannel.streamUpdates { + callback(it?.asJs(chatJs)) + } + return closeable::close + } + + fun sendText(text: String, options: SendTextOptionParams?): Promise { + @Suppress("USELESS_CAST") // cast required to be able to call "let" extension function + val files = (options?.files as? Any)?.let { files -> + val filesArray = + files as? Array<*> ?: arrayOf(files) + filesArray.filterNotNull().map { file -> + InputFile("", file.asDynamic().type ?: file.asDynamic().mimeType ?: "", UploadableImpl(file)) + } + } ?: listOf() + return baseChannel.sendText( + text = text, + meta = options?.meta?.unsafeCast>()?.toMap(), + shouldStore = options?.storeInHistory ?: true, + usePost = options?.sendByPost ?: false, + ttl = options?.ttl?.toInt(), + mentionedUsers = options?.mentionedUsers?.toMap()?.mapKeys { it.key.toInt() }, + referencedChannels = options?.referencedChannels?.toMap()?.mapKeys { it.key.toInt() }, + textLinks = options?.textLinks?.toList(), + quotedMessage = options?.quotedMessage?.baseMessage, + files = files, + customPushData = options?.customPushData?.toMap() + ).then { result -> + result.toPublishResponse() + }.asPromise() + } + + fun forwardMessage(message: BaseMessageJs): Promise { + return baseChannel.forwardMessage(message.baseMessage).then { it.toPublishResponse() }.asPromise() + } + + fun startTyping(): Promise { + return baseChannel.startTyping().then { it?.toPublishResponse() ?: undefined }.asPromise() + } + + fun stopTyping(): Promise { + return baseChannel.stopTyping().then { it?.toPublishResponse() ?: undefined }.asPromise() + } + + fun getTyping(callback: (Array) -> Unit): () -> Unit { + return baseChannel.getTyping { callback(it.toTypedArray()) } + .let { autoCloseable -> + autoCloseable::close + } + } + + fun connect(callback: (BaseMessageJs) -> Unit): () -> Unit { + return baseChannel.connect { + callback(it.asJs(chatJs)) + }::close + } + + fun whoIsPresent(): Promise> { + return baseChannel.whoIsPresent().then { it.toTypedArray() }.asPromise() + } + + fun isPresent(userId: String): Promise { + return baseChannel.isPresent(userId).asPromise() + } + + fun streamPresence(callback: (Array) -> Unit): Promise<() -> Unit> { + return baseChannel.streamPresence { callback(it.toTypedArray()) }.let { + it::close + }.asFuture().asPromise() + } + + open fun getHistory(params: GetHistoryParams?): Promise { + return baseChannel.getHistory( + params?.startTimetoken?.toLong(), + params?.endTimetoken?.toLong(), + params?.count?.toInt() ?: 25 + ).then { result -> + createJsObject { + this.isMore = result.isMore + this.messages = result.messages.map { it.asJs(chatJs) }.toTypedArray() + } + }.asPromise() + } + + fun getMessage(timetoken: String): Promise { + return baseChannel.getMessage(timetoken.tryLong()!!).then { it!!.asJs(chatJs) }.asPromise() + } + + open fun pinMessage(message: BaseMessageJs): Promise { + return baseChannel.pinMessage(message.baseMessage).then { it.asJs(chatJs) }.asPromise() + } + + open fun unpinMessage(): Promise { + return baseChannel.unpinMessage().then { it.asJs(chatJs) }.asPromise() + } + + fun getPinnedMessage(): Promise { + return baseChannel.getPinnedMessage().then { it?.asJs(chatJs) }.asPromise() + } + + fun createMessageDraft(config: MessageDraftConfig?): MessageDraftV1Js { + return MessageDraftV1Js( + chatJs, + this, + config + ) + } + + fun createMessageDraftV2(config: MessageDraftConfig?): MessageDraftV2Js { + return MessageDraftV2Js( + this.chatJs, + MessageDraftImpl( + this.baseChannel, + config?.userSuggestionSource?.let { + userSuggestionSourceFrom(it) + } ?: MessageDraft.UserSuggestionSource.CHANNEL, + config?.isTypingIndicatorTriggered ?: (baseChannel.type != ChannelType.PUBLIC), + config?.userLimit ?: 10, + config?.channelLimit ?: 10 + ), + createJsObject { + this.userSuggestionSource = config?.userSuggestionSource ?: "channel" + this.isTypingIndicatorTriggered = config?.isTypingIndicatorTriggered ?: (baseChannel.type != ChannelType.PUBLIC) + this.userLimit = config?.userLimit ?: 10 + this.channelLimit = config?.channelLimit ?: 10 + } + ) + } + + fun registerForPush(): Promise { + return baseChannel.registerForPush().asPromise() + } + + fun unregisterFromPush(): Promise { + return baseChannel.unregisterFromPush().asPromise() + } + + fun getFiles(params: PubNub.ListFilesParameters?): Promise { + return baseChannel.getFiles( + params?.limit?.toInt() ?: 100, + params?.next + ).then { result -> + createJsObject { + this.files = result.files.toTypedArray() + this.next = result.next + this.total = result.total + } + }.asPromise() + } + + fun deleteFile(params: DeleteFileParams): Promise { + return baseChannel.deleteFile(params.id, params.name).then { + createJsObject { + this.status = it.status + } + }.asPromise() + } + + fun setRestrictions(user: UserJs, params: RestrictionJs): Promise { + return baseChannel.setRestrictions( + user.user, + params.ban ?: false, + params.mute ?: false, + params.reason?.toString() + ).asPromise() + } + + fun getUserRestrictions(user: UserJs): Promise { + return baseChannel.getUserRestrictions(user.user).then { it.asJs() }.asPromise() + } + + fun getUsersRestrictions(params: PubNub.GetChannelMembersParameters?): Promise { + return baseChannel.getUsersRestrictions( + params?.limit?.toInt(), + params?.page?.toKmp(), + extractSortKeys(params?.sort), + ).then { + it.toJs() + }.asPromise() + } + + fun streamMessageReports(callback: (EventJs) -> Unit): () -> Unit = + baseChannel.streamMessageReports { event -> + callback(event.toJs(chatJs)) + }::close + + @Deprecated("Only for internal MessageDraft V1 use") + fun getUserSuggestions(text: String, options: GetSuggestionsParams?): Promise> { + val limit = options?.limit + val cacheKey = MessageElementsUtils.getPhraseToLookFor(text) ?: return Promise.resolve(emptyArray()) + return baseChannel.getUserSuggestions(cacheKey, limit?.toInt() ?: 10).then { memberships -> + memberships.map { it.asJs(chatJs) }.toTypedArray() + }.asPromise() + } + + fun getMessageReportsHistory(params: GetHistoryParams?): Promise { + return this.baseChannel.getMessageReportsHistory( + params?.startTimetoken?.toLong(), + params?.endTimetoken?.toLong(), + params?.count ?: 100 + ).then { result -> + createJsObject { + events = result.events.map { it.toJs(chatJs) }.toTypedArray() + isMore = result.isMore + } + }.asPromise() + } + + fun toJSON(): Json { + return json( + "id" to id, + "name" to name, + "custom" to custom, + "description" to description, + "updated" to updated, + "status" to status, + "type" to type + ) + } +} + +internal fun BaseChannel<*, *>.asJs(chat: ChatJs): BaseChannelJs = when (this) { + is Channel -> ChannelJs(this, chat) + is ThreadChannel -> ThreadChannelJs(this, chat) + else -> error("Unexpected error. $this is not a `Channel` or `ThreadChannel`") +} + +private fun userSuggestionSourceFrom(lowercaseString: String): MessageDraft.UserSuggestionSource { + return MessageDraft.UserSuggestionSource.entries.first { it.name.lowercase() == lowercaseString } +} diff --git a/pubnub-chat-impl/src/jsMain/kotlin/BaseMessageJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/BaseMessageJs.kt new file mode 100644 index 00000000..43b57921 --- /dev/null +++ b/pubnub-chat-impl/src/jsMain/kotlin/BaseMessageJs.kt @@ -0,0 +1,154 @@ +@file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) + +import com.pubnub.api.PubNubError +import com.pubnub.api.adjustCollectionTypes +import com.pubnub.chat.BaseMessage +import com.pubnub.chat.Message +import com.pubnub.chat.ThreadMessage +import com.pubnub.chat.internal.MessageDraftImpl +import com.pubnub.chat.types.MessageMentionedUser +import com.pubnub.chat.types.MessageReferencedChannel +import com.pubnub.kmp.JsMap +import com.pubnub.kmp.createJsObject +import com.pubnub.kmp.then +import com.pubnub.kmp.toJsMap +import com.pubnub.kmp.toMap +import kotlin.js.Json +import kotlin.js.Promise +import kotlin.js.json + +@JsExport +@JsName("BaseMessage") +open class BaseMessageJs internal constructor(internal val baseMessage: BaseMessage<*, *>, internal val chatJs: ChatJs) { + val timetoken: String get() = baseMessage.timetoken.toString() + val content: JsMap + get() { + return baseMessage.content.toJsTextMessage() + } + val channelId by baseMessage::channelId + val userId by baseMessage::userId + val actions get() = baseMessage.actions?.mapValues { + it.value.mapValues { + it.value.map { action -> + createJsObject { + uuid = action.uuid + actionTimetoken = action.actionTimetoken.toString() + } + }.toTypedArray() + }.toJsMap() + }?.toJsMap() + val meta get() = baseMessage.meta?.adjustCollectionTypes() + val error: String? get() { + return if (baseMessage.error == PubNubError.CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED) { + "Error while decrypting message content" + } else { + null + } + } + + val mentionedUsers: JsMap? + get() = baseMessage.mentionedUsers?.mapKeys { it.key.toString() }?.toJsMap() + val referencedChannels: JsMap? + get() = baseMessage.referencedChannels?.mapKeys { it.key.toString() }?.toJsMap() + val textLinks get() = baseMessage.textLinks?.toTypedArray() + val type by baseMessage::type + val quotedMessage: QuotedMessageJs? get() = baseMessage.quotedMessage?.toJs() + val files get() = baseMessage.files.toTypedArray() + val text by baseMessage::text + val deleted by baseMessage::deleted + val reactions: JsMap> + get() = baseMessage.reactions.mapValues { mapEntry -> + mapEntry.value.map { action -> + createJsObject { + uuid = action.uuid + actionTimetoken = action.actionTimetoken.toString() + } + }.toTypedArray() + }.toJsMap() + + fun streamUpdates(callback: (BaseMessageJs?) -> Unit): () -> Unit { + return baseMessage.streamUpdates { it.asJs(chatJs) }::close + } + + fun getLinkedText() = getMessageElements() + + fun getMessageElements(): Array { + // data from v1 message draft + if (mentionedUsers?.toMap()?.isNotEmpty() == true || + textLinks?.isNotEmpty() == true || + referencedChannels?.toMap()?.isNotEmpty() == true + ) { + return MessageElementsUtils.getMessageElements( + text, + mentionedUsers?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), + textLinks?.toList() ?: emptyList(), + referencedChannels?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), + ) + } else { + // use v2 message draft + return MessageDraftImpl.getMessageElements(text).toJs() + } + } + + fun editText(newText: String): Promise { + return baseMessage.editText(newText).then { it.asJs(chatJs) }.asPromise() + } + + fun delete(params: DeleteParameters?): Promise { + return baseMessage.delete(params?.soft ?: false, params?.asDynamic()?.preserveFiles ?: false) + .then { + it?.asJs(chatJs) ?: true + }.asPromise() + } + + fun restore(): Promise { + return baseMessage.restore().then { it.asJs(chatJs) }.asPromise() + } + + fun hasUserReaction(reaction: String): Boolean { + return baseMessage.hasUserReaction(reaction) + } + + fun toggleReaction(reaction: String): Promise { + return baseMessage.toggleReaction(reaction).then { it.asJs(chatJs) }.asPromise() + } + + fun forward(channelId: String): Promise { + return baseMessage.forward(channelId).then { it.toPublishResponse() }.asPromise() + } + + fun pin(): Promise { + return baseMessage.pin().then { it.asJs(chatJs) }.asPromise() + } + + fun report(reason: String): Promise { + return baseMessage.report(reason).then { it.toPublishResponse() }.asPromise() + } + + fun toJSON(): Json { + return json( + "timetoken" to timetoken, + "content" to content, + "channelId" to channelId, + "userId" to userId, + "actions" to actions, + "meta" to meta, + "mentionedUsers" to mentionedUsers, + "referencedChannels" to referencedChannels, + "textLinks" to textLinks.contentToString(), + "type" to type, + "quotedMessage" to quotedMessage, + "files" to files.contentToString(), + "text" to text, + "deleted" to deleted, + "reactions" to reactions, + "error" to error + ) + } +} + +internal fun BaseMessage<*, *>.asJs(chat: ChatJs) = when (this) { + is Message -> MessageJs(this, chat) + is ThreadMessage -> ThreadMessageJs(this, chat) + else -> error("Unexpected error. $this is not a `Message` or `ThreadMessage`") +} diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt index 322babb4..da1198a0 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ChannelJs.kt @@ -1,145 +1,19 @@ -@file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) +@file:OptIn(ExperimentalJsExport::class) import com.pubnub.chat.Channel -import com.pubnub.chat.MessageDraft -import com.pubnub.chat.internal.MessageDraftImpl -import com.pubnub.chat.internal.channel.BaseChannel -import com.pubnub.chat.types.ChannelType -import com.pubnub.chat.types.InputFile +import com.pubnub.chat.internal.channel.BaseChannelImpl +import com.pubnub.chat.internal.channel.ChannelImpl import com.pubnub.kmp.JsMap -import com.pubnub.kmp.UploadableImpl import com.pubnub.kmp.asFuture import com.pubnub.kmp.createJsObject import com.pubnub.kmp.then import com.pubnub.kmp.toJsMap -import com.pubnub.kmp.toMap -import kotlin.js.Json import kotlin.js.Promise -import kotlin.js.json @JsExport @JsName("Channel") -open class ChannelJs internal constructor(internal val channel: Channel, internal val chatJs: ChatJs) : ChannelFields { - override val id: String by channel::id - override val name: String? by channel::name - override val custom: Any? get() = channel.custom?.toJsMap() - override val description: String? by channel::description - override val updated: String? by channel::updated - override val status: String? by channel::status - override val type: String? get() = channel.type?.stringValue - - fun update(data: ChannelFields): Promise { - return channel.update( - data.name, - data.custom?.let { convertToCustomObject(it) }, - data.description, - data.status, - data.type?.let { ChannelType.from(it) } - ).then { - it.asJs(chatJs) - }.asPromise() - } - - fun delete(options: DeleteParameters?): Promise { - return channel.delete(options?.soft ?: false).then { - if (it != null) { - DeleteChannelResult(it.asJs(chatJs)) - } else { - DeleteChannelResult(true) - } - }.asPromise() - } - - fun streamUpdates(callback: (channel: ChannelJs?) -> Unit): () -> Unit { - val closeable = channel.streamUpdates { - callback(it?.asJs(chatJs)) - } - return closeable::close - } - - fun sendText(text: String, options: SendTextOptionParams?): Promise { - @Suppress("USELESS_CAST") // cast required to be able to call "let" extension function - val files = (options?.files as? Any)?.let { files -> - val filesArray = - files as? Array<*> ?: arrayOf(files) - filesArray.filterNotNull().map { file -> - InputFile("", file.asDynamic().type ?: file.asDynamic().mimeType ?: "", UploadableImpl(file)) - } - } ?: listOf() - return channel.sendText( - text = text, - meta = options?.meta?.unsafeCast>()?.toMap(), - shouldStore = options?.storeInHistory ?: true, - usePost = options?.sendByPost ?: false, - ttl = options?.ttl?.toInt(), - mentionedUsers = options?.mentionedUsers?.toMap()?.mapKeys { it.key.toInt() }, - referencedChannels = options?.referencedChannels?.toMap()?.mapKeys { it.key.toInt() }, - textLinks = options?.textLinks?.toList(), - quotedMessage = options?.quotedMessage?.message, - files = files, - customPushData = options?.customPushData?.toMap() - ).then { result -> - result.toPublishResponse() - }.asPromise() - } - - fun forwardMessage(message: MessageJs): Promise { - return channel.forwardMessage(message.message).then { it.toPublishResponse() }.asPromise() - } - - fun startTyping(): Promise { - return channel.startTyping().then { it?.toPublishResponse() ?: undefined }.asPromise() - } - - fun stopTyping(): Promise { - return channel.stopTyping().then { it?.toPublishResponse() ?: undefined }.asPromise() - } - - fun getTyping(callback: (Array) -> Unit): () -> Unit { - return channel.getTyping { callback(it.toTypedArray()) } - .let { autoCloseable -> - autoCloseable::close - } - } - - fun connect(callback: (MessageJs) -> Unit): () -> Unit { - return channel.connect { - callback(it.asJs(chatJs)) - }::close - } - - fun whoIsPresent(): Promise> { - return channel.whoIsPresent().then { it.toTypedArray() }.asPromise() - } - - fun isPresent(userId: String): Promise { - return channel.isPresent(userId).asPromise() - } - - fun streamPresence(callback: (Array) -> Unit): Promise<() -> Unit> { - return channel.streamPresence { callback(it.toTypedArray()) }.let { - it::close - }.asFuture().asPromise() - } - - open fun getHistory(params: GetHistoryParams?): Promise { - return channel.getHistory( - params?.startTimetoken?.toLong(), - params?.endTimetoken?.toLong(), - params?.count?.toInt() ?: 25 - ).then { result -> - createJsObject { - this.isMore = result.isMore - this.messages = result.messages.map { it.asJs(chatJs) }.toTypedArray() - } - }.asPromise() - } - - fun getMessage(timetoken: String): Promise { - return channel.getMessage(timetoken.tryLong()!!).then { it!!.asJs(chatJs) }.asPromise() - } - - fun join(callback: (MessageJs) -> Unit, params: PubNub.SetMembershipsParameters?): Promise { +class ChannelJs internal constructor(private val channel: Channel, chatJs: ChatJs) : BaseChannelJs(channel, chatJs) { + fun join(callback: (BaseMessageJs) -> Unit, params: PubNub.SetMembershipsParameters?): Promise { return channel.join { callback(it.asJs(chatJs)) }.then { createJsObject { membership = it.membership.asJs(chatJs) @@ -179,55 +53,6 @@ open class ChannelJs internal constructor(internal val channel: Channel, interna }.asPromise() } - open fun pinMessage(message: MessageJs): Promise { - return channel.pinMessage(message.message).then { it.asJs(chatJs) }.asPromise() - } - - open fun unpinMessage(): Promise { - return channel.unpinMessage().then { it.asJs(chatJs) }.asPromise() - } - - fun getPinnedMessage(): Promise { - return channel.getPinnedMessage().then { it?.asJs(chatJs) }.asPromise() - } - - fun createMessageDraft(config: MessageDraftConfig?): MessageDraftV1Js { - return MessageDraftV1Js( - chatJs, - this, - config - ) - } - - fun createMessageDraftV2(config: MessageDraftConfig?): MessageDraftV2Js { - return MessageDraftV2Js( - this.chatJs, - MessageDraftImpl( - this.channel, - config?.userSuggestionSource?.let { - userSuggestionSourceFrom(it) - } ?: MessageDraft.UserSuggestionSource.CHANNEL, - config?.isTypingIndicatorTriggered ?: (channel.type != ChannelType.PUBLIC), - config?.userLimit ?: 10, - config?.channelLimit ?: 10 - ), - createJsObject { - this.userSuggestionSource = config?.userSuggestionSource ?: "channel" - this.isTypingIndicatorTriggered = config?.isTypingIndicatorTriggered ?: (channel.type != ChannelType.PUBLIC) - this.userLimit = config?.userLimit ?: 10 - this.channelLimit = config?.channelLimit ?: 10 - } - ) - } - - fun registerForPush(): Promise { - return channel.registerForPush().asPromise() - } - - fun unregisterFromPush(): Promise { - return channel.unregisterFromPush().asPromise() - } - fun streamReadReceipts(callback: (JsMap>) -> Unit): Promise<() -> Unit> { return channel.streamReadReceipts { callback( @@ -241,94 +66,14 @@ open class ChannelJs internal constructor(internal val channel: Channel, interna } } - fun getFiles(params: PubNub.ListFilesParameters?): Promise { - return channel.getFiles( - params?.limit?.toInt() ?: 100, - params?.next - ).then { result -> - createJsObject { - this.files = result.files.toTypedArray() - this.next = result.next - this.total = result.total - } - }.asPromise() - } - - fun deleteFile(params: DeleteFileParams): Promise { - return channel.deleteFile(params.id, params.name).then { - createJsObject { - this.status = it.status - } - }.asPromise() - } - - fun setRestrictions(user: UserJs, params: RestrictionJs): Promise { - return channel.setRestrictions( - user.user, - params.ban ?: false, - params.mute ?: false, - params.reason?.toString() - ).asPromise() - } - - fun getUserRestrictions(user: UserJs): Promise { - return channel.getUserRestrictions(user.user).then { it.asJs() }.asPromise() - } - - fun getUsersRestrictions(params: PubNub.GetChannelMembersParameters?): Promise { - return channel.getUsersRestrictions( - params?.limit?.toInt(), - params?.page?.toKmp(), - extractSortKeys(params?.sort), - ).then { - it.toJs() - }.asPromise() - } - - fun streamMessageReports(callback: (EventJs) -> Unit): () -> Unit = - channel.streamMessageReports { event -> - callback(event.toJs(chatJs)) - }::close - - @Deprecated("Only for internal MessageDraft V1 use") - fun getUserSuggestions(text: String, options: GetSuggestionsParams?): Promise> { - val limit = options?.limit - val cacheKey = MessageElementsUtils.getPhraseToLookFor(text) ?: return Promise.resolve(emptyArray()) - return channel.getUserSuggestions(cacheKey, limit?.toInt() ?: 10).then { memberships -> - memberships.map { it.asJs(chatJs) }.toTypedArray() - }.asPromise() - } - - fun getMessageReportsHistory(params: GetHistoryParams?): Promise { - return this.channel.getMessageReportsHistory( - params?.startTimetoken?.toLong(), - params?.endTimetoken?.toLong(), - params?.count ?: 100 - ).then { result -> - createJsObject { - events = result.events.map { it.toJs(chatJs) }.toTypedArray() - isMore = result.isMore - } - }.asPromise() - } - - fun toJSON(): Json { - return json( - "id" to id, - "name" to name, - "custom" to custom, - "description" to description, - "updated" to updated, - "status" to status, - "type" to type - ) - } - companion object { @JsStatic fun streamUpdatesOn(channels: Array, callback: (Array) -> Unit): () -> Unit { val chatJs = channels.first().chatJs - val closeable = BaseChannel.streamUpdatesOn(channels.map { jsChannel -> jsChannel.channel }) { + val closeable = BaseChannelImpl.streamUpdatesOn( + channels.map { jsChannel -> jsChannel.channel }, + ChannelImpl::fromDTO + ) { callback(it.map { kmpChannel -> ChannelJs(kmpChannel, chatJs) }.toTypedArray()) } return closeable::close @@ -337,7 +82,3 @@ open class ChannelJs internal constructor(internal val channel: Channel, interna } internal fun Channel.asJs(chat: ChatJs) = ChannelJs(this, chat) - -private fun userSuggestionSourceFrom(lowercaseString: String): MessageDraft.UserSuggestionSource { - return MessageDraft.UserSuggestionSource.entries.first { it.name.lowercase() == lowercaseString } -} diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt index 74787b19..4953757f 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt @@ -152,11 +152,11 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig }.asPromise() } - fun getChannel(id: String): Promise { + fun getChannel(id: String): Promise { return chat.getChannel(id).then { it?.asJs(this) }.asPromise() } - fun updateChannel(id: String, data: ChannelFields): Promise { + fun updateChannel(id: String, data: ChannelFields): Promise { return chat.updateChannel( id, data.name, @@ -194,7 +194,7 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig } // internal - fun createChannel(id: String, data: PubNub.ChannelMetadata?): Promise { + fun createChannel(id: String, data: PubNub.ChannelMetadata?): Promise { return chat.createChannel( id, data?.name, @@ -209,7 +209,7 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig fun getThreadId(channelId: String, messageId: String) = ChatImpl.getThreadId(channelId, messageId.toLong()) - fun createPublicConversation(params: CreateChannelParams?): Promise { + fun createPublicConversation(params: CreateChannelParams?): Promise { val channelId: String? = params?.channelId val data: PubNub.ChannelMetadata? = params?.channelData return chat.createPublicConversation( @@ -390,11 +390,11 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig } @Deprecated("Only for internal MessageDraft V1 use") - fun getChannelSuggestions(text: String, options: GetSuggestionsParams?): Promise> { + fun getChannelSuggestions(text: String, options: GetSuggestionsParams?): Promise> { val limit = options?.limit - val cacheKey = MessageElementsUtils.getChannelPhraseToLookFor(text) ?: return Promise.resolve(emptyArray()) + val cacheKey = MessageElementsUtils.getChannelPhraseToLookFor(text) ?: return Promise.resolve(emptyArray()) return chat.getChannelSuggestions(cacheKey, limit?.toInt() ?: 10).then { channels -> - channels.map { it.asJs(this@ChatJs) }.toTypedArray() + channels.map { it.asJs(this@ChatJs) as BaseChannelJs }.toTypedArray() }.asPromise() } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/CryptoUtils.kt b/pubnub-chat-impl/src/jsMain/kotlin/CryptoUtils.kt index 37d57580..5efefc4a 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/CryptoUtils.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/CryptoUtils.kt @@ -2,7 +2,7 @@ import com.pubnub.api.createJsonElement import com.pubnub.chat.ThreadMessage -import com.pubnub.chat.internal.message.BaseMessage +import com.pubnub.chat.internal.message.BaseMessageImpl import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.types.EventContent @@ -10,11 +10,11 @@ import com.pubnub.chat.types.EventContent class CryptoUtils { companion object { @JsStatic - fun decrypt(params: DecryptParams): MessageJs { - val decryptedContentJs = params.decryptor(params.message.message.text) + fun decrypt(params: DecryptParams): BaseMessageJs { + val decryptedContentJs = params.decryptor(params.message.baseMessage.text) val decryptedContent: EventContent.TextMessageContent = PNDataEncoder.decode(createJsonElement(decryptedContentJs)) - val message = params.message.message as BaseMessage<*> + val message = params.message.baseMessage as BaseMessageImpl<*, *> val newMessage = message.copyWithContent(decryptedContent) return (newMessage as? ThreadMessage)?.asJs(params.chat) ?: newMessage.asJs(params.chat) } @@ -23,6 +23,6 @@ class CryptoUtils { external interface DecryptParams { val chat: ChatJs - val message: MessageJs + val message: BaseMessageJs val decryptor: (String) -> Any } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt index 363aba89..532bb2c8 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MembershipJs.kt @@ -11,7 +11,7 @@ import kotlin.js.json @JsExport @JsName("Membership") class MembershipJs internal constructor(internal val membership: Membership, internal val chatJs: ChatJs) { - val channel: ChannelJs get() = membership.channel.asJs(chatJs) + val channel: BaseChannelJs get() = membership.channel.asJs(chatJs) val user: UserJs get() = membership.user.asJs(chatJs) val custom get() = membership.custom?.toJsMap() val updated by membership::updated @@ -33,8 +33,8 @@ class MembershipJs internal constructor(internal val membership: Membership, int } } - fun setLastReadMessage(message: MessageJs): Promise { - return membership.setLastReadMessage(message.message).then { it.asJs(chatJs) }.asPromise() + fun setLastReadMessage(message: BaseMessageJs): Promise { + return membership.setLastReadMessage(message.baseMessage).then { it.asJs(chatJs) }.asPromise() } fun setLastReadMessageTimetoken(timetoken: String): Promise { diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV1Js.kt b/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV1Js.kt index 4822df36..985ed645 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV1Js.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV1Js.kt @@ -347,15 +347,15 @@ fun range(start: Int, stop: Int, step: Int = 1): List { @JsExport @JsName("MessageDraft") -class MessageDraftV1Js(private val chat: ChatJs, private val channel: ChannelJs, config: MessageDraftConfig? = null) { +class MessageDraftV1Js(private val chat: ChatJs, private val channel: BaseChannelJs, config: MessageDraftConfig? = null) { private var previousValue = "" private val mentionedUsers: MutableMap = mutableMapOf() - private val referencedChannels: MutableMap = mutableMapOf() + private val referencedChannels: MutableMap = mutableMapOf() private val _textLinks: MutableList = mutableListOf() var value = "" val textLinks get() = _textLinks.toTypedArray() - var quotedMessage: MessageJs? = null + var quotedMessage: BaseMessageJs? = null val config: MessageDraftConfig = createJsObject { this.userSuggestionSource = config?.userSuggestionSource ?: "channel" this.isTypingIndicatorTriggered = config?.isTypingIndicatorTriggered ?: true @@ -621,7 +621,7 @@ class MessageDraftV1Js(private val chat: ChatJs, private val channel: ChannelJs, copiedObject.forEach { (key, value) -> val referencedName = when (value) { is UserJs -> value.name.orEmpty() - is ChannelJs -> value.name.orEmpty() + is BaseChannelJs -> value.name.orEmpty() else -> error("Not going to happen") } @@ -690,13 +690,13 @@ class MessageDraftV1Js(private val chat: ChatJs, private val channel: ChannelJs, val referencesObject = result.referencesObject referencedChannels.clear() - referencedChannels.putAll(referencesObject.mapValues { it.value as ChannelJs }) + referencedChannels.putAll(referencesObject.mapValues { it.value as BaseChannelJs }) if (differentReference == null) { return Promise.resolve( mapOf( "channelOccurrenceIndex" to -1, - "suggestedChannels" to arrayOf() + "suggestedChannels" to arrayOf() ).toJsMap() ) } @@ -770,7 +770,7 @@ class MessageDraftV1Js(private val chat: ChatJs, private val channel: ChannelJs, value = result.trim() } - fun addReferencedChannel(channel: ChannelJs, channelNameOccurrenceIndex: Int) { + fun addReferencedChannel(channel: BaseChannelJs, channelNameOccurrenceIndex: Int) { var counter = 0 var result = "" var isChannelFound = false @@ -925,7 +925,7 @@ class MessageDraftV1Js(private val chat: ChatJs, private val channel: ChannelJs, ) } - fun addQuote(message: MessageJs) { + fun addQuote(message: BaseMessageJs) { if (message.channelId != channel.id) { throw PubNubException("You cannot quote messages from other channels") } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt b/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt index bdca582b..deea4ad1 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MessageDraftV2Js.kt @@ -20,12 +20,12 @@ class MessageDraftV2Js internal constructor( private val messageDraft: MessageDraftImpl, val config: MessageDraftConfig, ) { - val channel: ChannelJs get() = messageDraft.channel.asJs(chat) + val channel: BaseChannelJs get() = messageDraft.channel.asJs(chat) val value: String get() = messageDraft.value.toString() - var quotedMessage: MessageJs? = null + var quotedMessage: BaseMessageJs? = null var files: Any? = null - fun addQuote(message: MessageJs) { + fun addQuote(message: BaseMessageJs) { quotedMessage = message } @@ -59,7 +59,7 @@ class MessageDraftV2Js internal constructor( val name = file.name messageDraft.files.add(InputFile(name ?: "", type ?: "", UploadableImpl(file))) } - messageDraft.quotedMessage = quotedMessage?.message + messageDraft.quotedMessage = quotedMessage?.baseMessage return messageDraft.send( options?.meta?.unsafeCast>()?.toMap(), diff --git a/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt index 38dc3152..8b788bff 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/MessageJs.kt @@ -1,129 +1,14 @@ @file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) -import com.pubnub.api.PubNubError -import com.pubnub.api.adjustCollectionTypes import com.pubnub.chat.Message -import com.pubnub.chat.internal.MessageDraftImpl -import com.pubnub.chat.internal.message.BaseMessage -import com.pubnub.chat.types.MessageMentionedUser -import com.pubnub.chat.types.MessageReferencedChannel -import com.pubnub.kmp.JsMap -import com.pubnub.kmp.createJsObject +import com.pubnub.chat.internal.message.BaseMessageImpl import com.pubnub.kmp.then -import com.pubnub.kmp.toJsMap -import com.pubnub.kmp.toMap -import kotlin.js.Json import kotlin.js.Promise -import kotlin.js.json @JsExport @JsName("Message") -open class MessageJs internal constructor(internal val message: Message, internal val chatJs: ChatJs) { +class MessageJs internal constructor(private val message: Message, chatJs: ChatJs) : BaseMessageJs(message, chatJs) { val hasThread by message::hasThread - val timetoken: String get() = message.timetoken.toString() - val content: JsMap - get() { - return message.content.toJsTextMessage() - } - val channelId by message::channelId - val userId by message::userId - val actions get() = message.actions?.mapValues { - it.value.mapValues { - it.value.map { action -> - createJsObject { - uuid = action.uuid - actionTimetoken = action.actionTimetoken.toString() - } - }.toTypedArray() - }.toJsMap() - }?.toJsMap() - val meta get() = message.meta?.adjustCollectionTypes() - val error: String? get() { - return if (message.error == PubNubError.CRYPTO_IS_CONFIGURED_BUT_MESSAGE_IS_NOT_ENCRYPTED) { - "Error while decrypting message content" - } else { - null - } - } - - val mentionedUsers: JsMap? - get() = message.mentionedUsers?.mapKeys { it.key.toString() }?.toJsMap() - val referencedChannels: JsMap? - get() = message.referencedChannels?.mapKeys { it.key.toString() }?.toJsMap() - val textLinks get() = message.textLinks?.toTypedArray() - val type by message::type - val quotedMessage: QuotedMessageJs? get() = message.quotedMessage?.toJs() - val files get() = message.files.toTypedArray() - val text by message::text - val deleted by message::deleted - val reactions: JsMap> - get() = message.reactions.mapValues { mapEntry -> - mapEntry.value.map { action -> - createJsObject { - uuid = action.uuid - actionTimetoken = action.actionTimetoken.toString() - } - }.toTypedArray() - }.toJsMap() - - fun streamUpdates(callback: (MessageJs?) -> Unit): () -> Unit { - return message.streamUpdates { it.asJs(chatJs) }::close - } - - fun getLinkedText() = getMessageElements() - - fun getMessageElements(): Array { - // data from v1 message draft - if (mentionedUsers?.toMap()?.isNotEmpty() == true || - textLinks?.isNotEmpty() == true || - referencedChannels?.toMap()?.isNotEmpty() == true - ) { - return MessageElementsUtils.getMessageElements( - text, - mentionedUsers?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), - textLinks?.toList() ?: emptyList(), - referencedChannels?.toMap()?.mapKeys { it.key.toInt() } ?: emptyMap(), - ) - } else { - // use v2 message draft - return MessageDraftImpl.getMessageElements(text).toJs() - } - } - - fun editText(newText: String): Promise { - return message.editText(newText).then { it.asJs(chatJs) }.asPromise() - } - - fun delete(params: DeleteParameters?): Promise { - return message.delete(params?.soft ?: false, params?.asDynamic()?.preserveFiles ?: false) - .then { - it?.asJs(chatJs) ?: true - }.asPromise() - } - - fun restore(): Promise { - return message.restore().then { it.asJs(chatJs) }.asPromise() - } - - fun hasUserReaction(reaction: String): Boolean { - return message.hasUserReaction(reaction) - } - - fun toggleReaction(reaction: String): Promise { - return message.toggleReaction(reaction).then { it.asJs(chatJs) }.asPromise() - } - - fun forward(channelId: String): Promise { - return message.forward(channelId).then { it.toPublishResponse() }.asPromise() - } - - fun pin(): Promise { - return message.pin().then { it.asJs(chatJs) }.asPromise() - } - - fun report(reason: String): Promise { - return message.report(reason).then { it.toPublishResponse() }.asPromise() - } fun getThread(): Promise { return message.getThread().then { it.asJs(chatJs) }.asPromise() @@ -143,37 +28,13 @@ open class MessageJs internal constructor(internal val message: Message, interna }.asPromise() } - fun toJSON(): Json { - return json( - "hasThread" to hasThread, - "timetoken" to timetoken, - "content" to content, - "channelId" to channelId, - "userId" to userId, - "actions" to actions, - "meta" to meta, - "mentionedUsers" to mentionedUsers, - "referencedChannels" to referencedChannels, - "textLinks" to textLinks.contentToString(), - "type" to type, - "quotedMessage" to quotedMessage, - "files" to files.contentToString(), - "text" to text, - "deleted" to deleted, - "reactions" to reactions, - "error" to error - ) - } - companion object { @JsStatic fun streamUpdatesOn(messages: Array, callback: (Array) -> Unit): () -> Unit { val chatJs = messages.first().chatJs - return BaseMessage.streamUpdatesOn(messages.map { it.message }) { kmpMessages -> - callback(kmpMessages.map { kmpMessage -> kmpMessage.asJs(chatJs) }.toTypedArray()) + return BaseMessageImpl.streamUpdatesOn(messages.map { it.message }) { kmpMessages -> + callback(kmpMessages.map { kmpMessage -> MessageJs(kmpMessage, chatJs) }.toTypedArray()) }::close } } } - -internal fun Message.asJs(chat: ChatJs) = MessageJs(this, chat) diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt index 418dcbb9..1870307e 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ThreadChannelJs.kt @@ -14,22 +14,25 @@ import kotlin.js.Promise @JsExport @JsName("ThreadChannel") -class ThreadChannelJs internal constructor(internal val threadChannel: ThreadChannel, chatJs: ChatJs) : ChannelJs(threadChannel, chatJs) { +class ThreadChannelJs internal constructor( + internal val threadChannel: ThreadChannel, + chatJs: ChatJs +) : BaseChannelJs(threadChannel, chatJs) { val parentChannelId by threadChannel::parentChannelId - override fun pinMessage(message: MessageJs): Promise { - return channel.pinMessage(message.message).then { it.asJs(chatJs) }.asPromise() + override fun pinMessage(message: BaseMessageJs): Promise { + return baseChannel.pinMessage(message.baseMessage).then { it.asJs(chatJs) }.asPromise() } - override fun unpinMessage(): Promise { - return channel.unpinMessage().then { it.asJs(chatJs) }.asPromise() + override fun unpinMessage(): Promise { + return baseChannel.unpinMessage().then { it.asJs(chatJs) }.asPromise() } - fun pinMessageToParentChannel(message: ThreadMessageJs): Promise { + fun pinMessageToParentChannel(message: ThreadMessageJs): Promise { return threadChannel.pinMessageToParentChannel(message.threadMessage).then { it.asJs(chatJs) }.asPromise() } - fun unpinMessageFromParentChannel(): Promise { + fun unpinMessageFromParentChannel(): Promise { return threadChannel.unpinMessageFromParentChannel().then { it.asJs(chatJs) }.asPromise() } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ThreadMessageJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ThreadMessageJs.kt index aac5823b..5d47a30a 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ThreadMessageJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ThreadMessageJs.kt @@ -1,13 +1,16 @@ @file:OptIn(ExperimentalJsExport::class, ExperimentalJsStatic::class) import com.pubnub.chat.ThreadMessage -import com.pubnub.chat.internal.message.BaseMessage +import com.pubnub.chat.internal.message.BaseMessageImpl import com.pubnub.kmp.then import kotlin.js.Promise @JsExport @JsName("ThreadMessage") -class ThreadMessageJs internal constructor(internal val threadMessage: ThreadMessage, chatJs: ChatJs) : MessageJs(threadMessage, chatJs) { +class ThreadMessageJs internal constructor( + internal val threadMessage: ThreadMessage, + chatJs: ChatJs +) : BaseMessageJs(threadMessage, chatJs) { val parentChannelId by threadMessage::parentChannelId fun pinToParentChannel(): Promise { @@ -22,7 +25,7 @@ class ThreadMessageJs internal constructor(internal val threadMessage: ThreadMes @JsStatic fun streamUpdatesOn(threadMessages: Array, callback: (Array) -> Unit): () -> Unit { val chatJs = threadMessages.first().chatJs - return BaseMessage.streamUpdatesOn(threadMessages.map { it.threadMessage }) { messages -> + return BaseMessageImpl.streamUpdatesOn(threadMessages.map { it.threadMessage }) { messages -> callback(messages.map { it.asJs(chatJs) }.toTypedArray()) }::close } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt index fafa9fef..dbb3599d 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/UserJs.kt @@ -81,19 +81,19 @@ class UserJs internal constructor(internal val user: User, internal val chatJs: } fun setRestrictions( - channel: ChannelJs, + channel: BaseChannelJs, params: RestrictionJs, ): Promise { return user.setRestrictions( - channel.channel, + channel.baseChannel, params.ban ?: false, params.mute ?: false, params.reason?.toString() ).asPromise() } - fun getChannelRestrictions(channel: ChannelJs): Promise { - return user.getChannelRestrictions(channel.channel).then { + fun getChannelRestrictions(channel: BaseChannelJs): Promise { + return user.getChannelRestrictions(channel.baseChannel).then { it.asJs() }.asPromise() } diff --git a/pubnub-chat-impl/src/jsMain/kotlin/types.kt b/pubnub-chat-impl/src/jsMain/kotlin/types.kt index 3e312600..200914df 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/types.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/types.kt @@ -30,44 +30,44 @@ external interface GetCurrentUserMentionsResultJs { external interface UserMentionDataJs { val event: EventJs val userId: String - val message: MessageJs + val message: BaseMessageJs } external interface ChannelMentionDataJs : UserMentionDataJs { override var event: EventJs override var userId: String - override var message: MessageJs + override var message: BaseMessageJs var channelId: String } external interface ThreadMentionDataJs : UserMentionDataJs { override var event: EventJs override var userId: String - override var message: MessageJs + override var message: BaseMessageJs var parentChannelId: String var threadChannelId: String } external interface GetUnreadMessagesCountsJs { - var channel: ChannelJs + var channel: BaseChannelJs var membership: MembershipJs var count: Double } external interface CreateGroupConversationResultJs { - var channel: ChannelJs + var channel: BaseChannelJs var hostMembership: MembershipJs var inviteesMemberships: Array } external interface CreateDirectConversationResultJs { - var channel: ChannelJs + var channel: BaseChannelJs var hostMembership: MembershipJs var inviteeMembership: MembershipJs } external interface GetChannelsResponseJs { - var channels: Array + var channels: Array var page: PubNub.MetadataPage var total: Int } @@ -115,7 +115,7 @@ external interface MembersResponseJs { } external interface HistoryResponseJs { - var messages: Array + var messages: Array var isMore: Boolean } @@ -202,7 +202,7 @@ external interface SendTextOptionParams : PubNub.PublishParameters { var mentionedUsers: JsMap? var referencedChannels: JsMap? var textLinks: Array? - var quotedMessage: MessageJs? + var quotedMessage: BaseMessageJs? var files: Any? var customPushData: JsMap? } @@ -232,7 +232,7 @@ inline fun DeleteUserResult(boolean: Boolean): DeleteUserResult { external interface DeleteChannelResult @Suppress("NOTHING_TO_INLINE") -inline fun DeleteChannelResult(channel: ChannelJs): DeleteChannelResult { +inline fun DeleteChannelResult(channel: BaseChannelJs): DeleteChannelResult { return channel.unsafeCast() } diff --git a/src/commonMain/kotlin/com/pubnub/chat/mediators.kt b/src/commonMain/kotlin/com/pubnub/chat/mediators.kt index d1a5dddb..85f6808f 100644 --- a/src/commonMain/kotlin/com/pubnub/chat/mediators.kt +++ b/src/commonMain/kotlin/com/pubnub/chat/mediators.kt @@ -4,8 +4,9 @@ import com.pubnub.chat.MessageDraft.UserSuggestionSource import com.pubnub.chat.internal.MembershipImpl import com.pubnub.chat.internal.MessageDraftImpl import com.pubnub.chat.internal.UserImpl -import com.pubnub.chat.internal.channel.BaseChannel -import com.pubnub.chat.internal.message.BaseMessage +import com.pubnub.chat.internal.channel.BaseChannelImpl +import com.pubnub.chat.internal.channel.ChannelImpl +import com.pubnub.chat.internal.message.BaseMessageImpl import com.pubnub.chat.types.QuotedMessage /** @@ -18,7 +19,7 @@ import com.pubnub.chat.types.QuotedMessage fun Message.Companion.streamUpdatesOn( messages: Collection, callback: (messages: Collection) -> Unit, -): AutoCloseable = BaseMessage.streamUpdatesOn(messages, callback) +): AutoCloseable = BaseMessageImpl.streamUpdatesOn(messages, callback) /** * Receive updates when specific messages and related message reactions are added, edited, or removed. @@ -30,7 +31,7 @@ fun Message.Companion.streamUpdatesOn( fun ThreadMessage.Companion.streamUpdatesOn( messages: Collection, callback: (messages: Collection) -> Unit, -): AutoCloseable = BaseMessage.streamUpdatesOn(messages, callback) +): AutoCloseable = BaseMessageImpl.streamUpdatesOn(messages, callback) /** * Receives updates on list of [Channel] object. @@ -45,22 +46,7 @@ fun ThreadMessage.Companion.streamUpdatesOn( fun Channel.Companion.streamUpdatesOn( channels: Collection, callback: (channels: Collection) -> Unit, -): AutoCloseable = BaseChannel.streamUpdatesOn(channels, callback) - -/** - * Receives updates on list of [Channel] object. - * - * @param channels Collection of channels to get updates. - * @param callback Function that takes a single Channel object. It defines the custom behavior to be executed when - * detecting channel changes. - * - * @return [AutoCloseable] interface that lets you stop receiving channel-related updates (objects events) - * and clean up resources by invoking the close() method. - */ -fun ThreadChannel.Companion.streamUpdatesOn( - channels: Collection, - callback: (channels: Collection) -> Unit, -): AutoCloseable = BaseChannel.streamUpdatesOn(channels, callback) +): AutoCloseable = BaseChannelImpl.streamUpdatesOn(channels, ChannelImpl::fromDTO, callback) /** * You can receive updates when specific user-channel Membership object(s) are added, edited, or removed. diff --git a/src/jsMain/resources/index.d.ts b/src/jsMain/resources/index.d.ts index 7a78af43..23dafa92 100644 --- a/src/jsMain/resources/index.d.ts +++ b/src/jsMain/resources/index.d.ts @@ -335,8 +335,8 @@ type ThreadMentionData = { }; type UserMentionData = ChannelMentionData | ThreadMentionData; type MessageFields = Pick; -declare class Message { - protected chat: Chat; + +declare class BaseMessage { readonly timetoken: string; readonly content: TextMessageContent; readonly channelId: string; @@ -346,7 +346,6 @@ declare class Message { [key: string]: any; }; readonly error?: string; - get hasThread(): boolean; get mentionedUsers(): any; get referencedChannels(): any; get textLinks(): any; @@ -359,11 +358,6 @@ declare class Message { type?: string | undefined; }[]; /* - * Updates - */ - static streamUpdatesOn(messages: Message[], callback: (messages: Message[]) => unknown): () => void; - streamUpdates(callback: (message: Message) => unknown): () => void; - /* * Message text */ get text(): string; @@ -400,6 +394,18 @@ declare class Message { /** @deprecated */ DEPRECATED_report(reason: string): Promise; report(reason: string): Promise; +} + +declare class Message extends BaseMessage { + protected chat: Chat; + get hasThread(): boolean; + + /* + * Updates + */ + static streamUpdatesOn(messages: Message[], callback: (messages: Message[]) => unknown): () => void; + streamUpdates(callback: (message: Message) => unknown): () => void; + /** * Threads */ @@ -494,8 +500,8 @@ declare class MessageDraft { removeQuote(): void; } type ChannelFields = Pick; -declare class Channel { - protected chat: Chat; + +declare class BaseChannel { readonly id: string; readonly name?: string; readonly custom?: AppContext.CustomData; @@ -503,74 +509,32 @@ declare class Channel { readonly updated?: string; readonly status?: string; readonly type?: ChannelType; - /* - * CRUD - */ - update(data: Omit): Promise; - delete(options?: DeleteParameters): Promise; - /* - * Updates - */ - static streamUpdatesOn(channels: Channel[], callback: (channels: Channel[]) => unknown): () => void; - streamUpdates(callback: (channel: Channel) => unknown): () => void; + sendText(text: string, options?: SendTextOptionParams): Promise; - forwardMessage(message: Message): Promise; + forwardMessage(message: BaseMessage): Promise; startTyping(): Promise; stopTyping(): Promise; getTyping(callback: (typingUserIds: string[]) => unknown): () => void; /* - * Streaming messages - */ - connect(callback: (message: Message) => void): () => void; - /* * Presence */ whoIsPresent(): Promise; isPresent(userId: string): Promise; streamPresence(callback: (userIds: string[]) => unknown): Promise<() => void>; - /* - * Messages - */ - getHistory(params?: { - startTimetoken?: string; - endTimetoken?: string; - count?: number; - }): Promise<{ - messages: Message[]; - isMore: boolean; - }>; - getMessage(timetoken: string): Promise; - join(callback: (message: Message) => void, params?: Omit, "channels" | "include" | "filter"> & { - custom?: AppContext.CustomData; - }): Promise<{ - membership: Membership; - disconnect: () => void; - }>; + leave(): Promise; - getMembers(params?: Omit): Promise<{ - page: { - next: string | undefined; - prev: string | undefined; - }; - total: number | undefined; - status: number; - members: Membership[]; - }>; - invite(user: User): Promise; - inviteMultiple(users: User[]): Promise; pinMessage(message: Message): Promise; unpinMessage(): Promise; getPinnedMessage(): Promise; getUserSuggestions(text: string, options?: { limit: number; }): Promise; + createMessageDraft(config?: Partial): MessageDraft; createMessageDraftV2(config?: Partial): MessageDraftV2; registerForPush(): Promise; unregisterFromPush(): Promise; - streamReadReceipts(callback: (receipts: { - [key: string]: string[]; - }) => unknown): Promise<() => void>; + getFiles(params?: Omit): Promise<{ files: { name: string; @@ -624,6 +588,62 @@ declare class Channel { }>; streamMessageReports(callback: (event: Event<"report">) => void): () => void; } + +declare class Channel extends BaseChannel { + protected chat: Chat; + + /* + * CRUD + */ + update(data: Omit): Promise; + delete(options?: DeleteParameters): Promise; + /* + * Updates + */ + static streamUpdatesOn(channels: Channel[], callback: (channels: Channel[]) => unknown): () => void; + streamUpdates(callback: (channel: Channel) => unknown): () => void; + + /* + * Streaming messages + */ + connect(callback: (message: Message) => void): () => void; + + /* + * Messages + */ + getHistory(params?: { + startTimetoken?: string; + endTimetoken?: string; + count?: number; + }): Promise<{ + messages: Message[]; + isMore: boolean; + }>; + getMessage(timetoken: string): Promise; + join(callback: (message: Message) => void, params?: Omit, "channels" | "include" | "filter"> & { + custom?: AppContext.CustomData; + }): Promise<{ + membership: Membership; + disconnect: () => void; + }>; + + getMembers(params?: Omit): Promise<{ + page: { + next: string | undefined; + prev: string | undefined; + }; + total: number | undefined; + status: number; + members: Membership[]; + }>; + invite(user: User): Promise; + inviteMultiple(users: User[]): Promise; + + streamReadReceipts(callback: (receipts: { + [key: string]: string[]; + }) => unknown): Promise<() => void>; + +} type ChatConfig = { saveDebugLog: boolean; typingTimeout: number; @@ -783,7 +803,7 @@ declare class Chat { reason?: string; }): Promise; } -declare class ThreadMessage extends Message { +declare class ThreadMessage extends BaseMessage { readonly parentChannelId: string; static streamUpdatesOn(threadMessages: ThreadMessage[], callback: (threadMessages: ThreadMessage[]) => unknown): () => void; pinToParentChannel(): Promise;