taomenggo init

This commit is contained in:
guozhen
2024-08-06 10:30:15 +08:00
committed by xuhuixiang
parent 3e7fd07f4f
commit c929efd05e
3007 changed files with 229844 additions and 77 deletions

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.netease.yunxin.kit.conversationkit.ui"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<provider
android:name="com.netease.yunxin.kit.corekit.startup.InitializationProvider"
android:authorities="${applicationId}.xkit-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.netease.yunxin.kit.conversationkit.ui.ConversationUIService"
android:value="xkit.startup" />
</provider>
<activity android:name=".normal.page.ConversationSelectActivity"
android:screenOrientation="portrait" android:exported="false"/>
<activity android:name=".normal.page.ConversationActivity"
android:screenOrientation="portrait"
android:exported="false"/>
<activity android:name=".fun.page.FunConversationActivity"
android:screenOrientation="portrait"
android:exported="false"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,160 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.view.View;
import android.widget.TextView;
import com.netease.nim.highavailable.LogUtils;
import com.netease.nimlib.sdk.msg.attachment.NetCallAttachment;
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
import com.netease.nimlib.sdk.team.constant.TeamFieldEnum;
import com.netease.nimlib.sdk.team.model.UpdateTeamAttachment;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.conversationkit.ui.fun.viewholder.EmojiManager;
import com.netease.yunxin.kit.corekit.im.model.AttachmentContent;
import java.util.Map;
import java.util.regex.Matcher;
public class ConversationCustom {
public String customContentText(Context context, ConversationInfo conversationInfo) {
if (conversationInfo != null && context != null) {
MsgTypeEnum typeEnum = conversationInfo.getMsgType();
switch (typeEnum) {
case notification:
String result = context.getString(R.string.msg_type_notification);
if (conversationInfo.getAttachment() instanceof UpdateTeamAttachment) {
UpdateTeamAttachment attachment = (UpdateTeamAttachment) conversationInfo.getAttachment();
for (Map.Entry<TeamFieldEnum, Object> field : attachment.getUpdatedFields().entrySet()) {
if (field.getKey() == TeamFieldEnum.Announcement && !TextUtils.isEmpty(field.getValue().toString())) {
result = String.format(context.getString(R.string.msg_type_notification_announcement), field.getValue());
}
}
}
return result;
case text:
return conversationInfo.getContent();
case audio:
return context.getString(R.string.msg_type_audio);
case video:
return context.getString(R.string.msg_type_video);
case tip:
return context.getString(R.string.msg_type_tip);
case image:
return context.getString(R.string.msg_type_image);
case file:
return context.getString(R.string.msg_type_file);
case location:
return context.getString(R.string.msg_type_location);
case nrtc_netcall:
NetCallAttachment attachment = (NetCallAttachment) conversationInfo.getAttachment();
int type = attachment.getType();
if (type == 1) {
return context.getString(R.string.msg_type_rtc_audio);
} else {
return context.getString(R.string.msg_type_rtc_video);
}
case custom:
String resultcustom = conversationInfo.getContent();
if (conversationInfo.getAttachment() instanceof AttachmentContent) {
resultcustom = ((AttachmentContent) conversationInfo.getAttachment()).getContent();
}
return TextUtils.isEmpty(resultcustom) ? context.getString(R.string.msg_type_custom) : resultcustom;
default:
return context.getString(R.string.msg_type_no_tips);
}
}
return "";
}
//处理会话表情
public static final float DEF_SCALE = 0.2f;
public static void identifyExpression(Context context, View textView, String message) {
if (message != null && textView != null) {
SpannableString spannableString =
replaceEmoticons(context, message, DEF_SCALE, ImageSpan.ALIGN_BOTTOM);
// int color = context.getResources().getColor(R.color.color_007aff);
// identifyAitExpression(context, spannableString, color, message);
viewSetText(textView, spannableString);
}
}
private static void viewSetText(View textView, SpannableString mSpannableString) {
if (textView instanceof TextView) {
TextView tv = (TextView) textView;
tv.setText(mSpannableString);
}
}
private static Drawable getEmotDrawable(Context context, String text, float scale) {
Drawable drawable = EmojiManager.getDrawable(context, text);
// scale
if (drawable != null) {
String assPath = EmojiManager.getDrawablePath(context, text);
if (assPath != null && assPath.contains("tlt_gif_")) {
LogUtils.i("XXXX","XXXX:"+assPath);
int number = Integer.parseInt(assPath.replace("emoji/tlt/tlt_gif_","").replace(".png",""));
if(number>14){
int width = (int) (drawable.getIntrinsicWidth() * scale / 2.0f);
int height = (int) (drawable.getIntrinsicHeight() * scale / 2.0f);
drawable.setBounds(0, 0, width, height);
}else{
int width = (int) (drawable.getIntrinsicWidth() * scale / 3.8f);
int height = (int) (drawable.getIntrinsicHeight() * scale / 3.8f);
drawable.setBounds(0, 0, width, height);
}
}
else {
if (assPath.contains("default")) {
scale = 0.6f;
}
int width = (int) (drawable.getIntrinsicWidth() * scale);
int height = (int) (drawable.getIntrinsicHeight() * scale);
drawable.setBounds(0, 0, width, height);
}
}
return drawable;
}
private static SpannableString replaceEmoticons(
Context context, String value, float scale, int align) {
if (TextUtils.isEmpty(value)) {
value = "";
}
SpannableString mSpannableString = new SpannableString(value);
Matcher matcher = EmojiManager.getPattern().matcher(value);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
String emot = value.substring(start, end);
Drawable d = getEmotDrawable(context, emot, scale);
if (d != null) {
ImageSpan span = new ImageSpan(d, align);
mSpannableString.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return mSpannableString;
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
public class ConversationKitClient {
private static ConversationUIConfig sConversationConfig;
public static void setConversationUIConfig(ConversationUIConfig config) {
sConversationConfig = config;
}
public static ConversationUIConfig getConversationUIConfig() {
return sConversationConfig;
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
import android.graphics.drawable.Drawable;
import android.view.View;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import java.util.Comparator;
public class ConversationUIConfig {
public static Integer INT_DEFAULT_NULL = 0;
public boolean showTitleBar = true;
public boolean showTitleBarLeftIcon = true;
public boolean showTitleBarRightIcon = true;
public boolean showTitleBarRight2Icon = true;
public Integer titleBarLeftRes = null;
public Integer titleBarRightRes = null;
public Integer titleBarRight2Res = null;
public String titleBarTitle;
public Integer titleBarTitleColor = null;
public Integer itemTitleColor = null;
public Integer itemTitleSize = null;
public Integer itemContentColor = null;
public Integer itemContentSize = null;
public Integer itemDateColor = null;
public Integer itemDateSize = null;
public View.OnClickListener titleBarRightClick;
public View.OnClickListener titleBarRight2Click;
public View.OnClickListener titleBarLeftClick;
public ItemClickListener itemClickListener;
public Comparator<ConversationInfo> conversationComparator;
public IConversationFactory conversationFactory;
public Float avatarCornerRadius = null;
public Drawable itemStickTopBackground;
public Drawable itemBackground;
public ConversationCustom conversationCustom;
public IConversationViewLayout customLayout;
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
public class ConversationUIConstant {
public static final int MAX_TEAM_MEMBER = 200;
public static final int ERROR_CODE_NETWORK = 415;
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
import android.content.Context;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.chatkit.ChatService;
import com.netease.yunxin.kit.conversationkit.ui.fun.page.FunConversationActivity;
import com.netease.yunxin.kit.conversationkit.ui.normal.page.ConversationActivity;
import com.netease.yunxin.kit.corekit.im.utils.RouterConstant;
import com.netease.yunxin.kit.corekit.route.XKitRouter;
public class ConversationUIService extends ChatService {
@NonNull
@Override
public String getServiceName() {
return "ConversationUIKit";
}
@NonNull
@Override
public String getVersionName() {
return BuildConfig.versionName;
}
@NonNull
@Override
public ChatService create(@NonNull Context context) {
//normal
XKitRouter.registerRouter(RouterConstant.PATH_CONVERSATION_PAGE, ConversationActivity.class);
//fun
XKitRouter.registerRouter(
RouterConstant.PATH_FUN_CONVERSATION_PAGE, FunConversationActivity.class);
return this;
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public interface IConversationFactory {
//根据会话数据创建ViewHolder中数据类
ConversationBean CreateBean(ConversationInfo info);
//Adapter获取数据对应的ViewType
int getItemViewType(ConversationBean data);
//创建ViewHolder
BaseViewHolder<ConversationBean> createViewHolder(@NonNull ViewGroup parent, int viewType);
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
import com.netease.yunxin.kit.conversationkit.ui.page.ConversationBaseFragment;
public interface IConversationViewLayout {
void customizeConversationLayout(final ConversationBaseFragment fragment);
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui;
import android.content.Context;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public interface ItemClickListener {
default boolean onClick(Context context, ConversationBean data, int position) {
return false;
}
default boolean onLongClick(Context context, ConversationBean data, int position) {
return false;
}
default boolean onAvatarClick(Context context, ConversationBean data, int position) {
return false;
}
default boolean onAvatarLongClick(Context context, ConversationBean data, int position) {
return false;
}
}

View File

@@ -0,0 +1,136 @@
package com.netease.yunxin.kit.conversationkit.ui;
import android.content.Context;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
public class TimeConversationUtils {
public static String formatMillisecond(Context mcontext, long timeseconds) {
String ret = "";
Date srcDate = new Date(timeseconds);
try {
boolean isChineseLanguage = isChineseLanguage();
GregorianCalendar gcCurrent = new GregorianCalendar();
gcCurrent.setTime(new Date());
int currentYear = gcCurrent.get(1);
int currentMonth = gcCurrent.get(2) + 1;
int currentDay = gcCurrent.get(5);
GregorianCalendar gcSrc = new GregorianCalendar();
gcSrc.setTime(srcDate);
int srcYear = gcSrc.get(1);
int srcMonth = gcSrc.get(2) + 1;
int srcDay = gcSrc.get(5);
String timeExtraStr = "";
timeExtraStr = " " + getTimeHH24Human(srcDate, true);
if (currentYear == srcYear) {
long currentTimestamp = gcCurrent.getTimeInMillis();
long srcTimestamp = gcSrc.getTimeInMillis();
long delta = currentTimestamp - srcTimestamp;
if (currentMonth == srcMonth && currentDay == srcDay) {
ret = getTimeHH24Human(srcDate, false);
} else {
GregorianCalendar yesterdayDate = new GregorianCalendar();
yesterdayDate.add(5, -1);
GregorianCalendar beforeYesterdayDate = new GregorianCalendar();
beforeYesterdayDate.add(5, -2);
if (srcMonth == yesterdayDate.get(2) + 1 && srcDay == yesterdayDate.get(5)) {
ret = mcontext.getString(R.string.time_zuotian_txt) + timeExtraStr;
}
//不用前天
// else if (srcMonth == beforeYesterdayDate.get(2) + 1 && srcDay == beforeYesterdayDate.get(5)) {
// ret = mcontext.getString(R.string.time_qiantian_txt) + timeExtraStr;
// }
else {
long deltaHour = delta / 3600000L;
if (deltaHour < 168L) {
String[] weekday = new String[]{"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
String weedayDesc = weekday[gcSrc.get(7) - 1];
ret = weedayDesc;
} else {
ret = getTimeString(srcDate, isChineseLanguage() ? "M月d日" : "M/d");
}
}
}
} else {
// ret = getTimeString(srcDate, isChineseLanguage ? "yy年M月d日" : "yy/M/d") + timeExtraStr;
ret = getTimeString(srcDate, isChineseLanguage ? "yyyy年M月d日" : "yyyy/M/d");
}
} catch (Exception var26) {
System.err.println("【DEBUG-getTimeStringAutoShort】计算出错" + var26.getMessage() + " 【NO】");
}
return ret;
}
public static String getTimeString(Date dt, String pattern) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
sdf.setTimeZone(TimeZone.getDefault());
return sdf.format(dt);
} catch (Exception var3) {
return "";
}
}
private static String getTimeHH24Human(Date srcDate, boolean timeWithSegmentStr) {
String ret = "";
try {
String timePattern = "HH:mm";
String timeStr = getTimeString(srcDate, timePattern);
// String timeSegmentStr = "";
// if (timeWithSegmentStr) {
// timeSegmentStr = isChineseLanguage() ? getTimeSegmentStr(timeStr) : "";
// }
// ret = timeSegmentStr + timeStr;
ret = timeStr;
} catch (Exception var6) {
System.err.println("【DEBUG-getTimeHH24Human】计算出错" + var6.getMessage() + " 【NO】");
}
return ret;
}
private static boolean isChineseLanguage() {
boolean is = true;
try {
is = Locale.getDefault().getLanguage().equals((new Locale("zh")).getLanguage());
} catch (Exception var2) {
System.err.println("【DEBUG-isChineseLanguage】" + var2.getMessage());
}
return is;
}
private static String getTimeSegmentStr(String hh24) {
String ret = "";
if (hh24 != null && hh24.length() >= 2) {
try {
int a = Integer.parseInt(hh24.substring(0, 2));
if (a >= 0 && a <= 6) {
ret = "凌晨";
} else if (a > 6 && a <= 12) {
ret = "上午";
} else if (a > 12 && a <= 13) {
ret = "中午";
} else if (a > 13 && a <= 18) {
ret = "下午";
} else if (a > 18 && a <= 24) {
ret = "晚上";
}
} catch (Exception var3) {
System.err.println("【DEBUG-getTimeSegmentStr】计算出错" + var3.getMessage() + " 【NO】");
}
}
return ret;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.common;
public class ConversationConstant {
public static final String LIB_TAG = "ConversationKit-UI";
public static class ViewType {
//p2p chat view
public static final int CHAT_VIEW = 1;
//team chat view
public static final int TEAM_VIEW = 2;
}
public static class Action {
//action to add or remove stick
public static final String ACTION_STICK = "conversation/action/stick";
//action to delete conversation
public static final String ACTION_DELETE = "conversation/action/delete";
public static final String ACTION_HIDE = "conversation/action/hide";
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.common;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ConversationHelper {
private static Map<String, Boolean> aitInfo = new HashMap<>();
public static void updateAitInfo(List<String> sessionIdList, boolean hasAit) {
if (sessionIdList != null) {
for (String sessionId : sessionIdList) {
aitInfo.put(sessionId, hasAit);
}
}
}
public static boolean hasAit(String sessionId) {
if (aitInfo.containsKey(sessionId)) {
return aitInfo.get(sessionId);
}
return false;
}
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.common;
import static com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant.LIB_TAG;
import android.content.Context;
import com.netease.nimlib.sdk.msg.attachment.NotificationAttachment;
import com.netease.nimlib.sdk.msg.constant.NotificationType;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.conversationkit.ui.ConversationCustom;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
public class ConversationUtils {
private static final String TAG = "ConversationUtils";
private static ConversationCustom custom = new ConversationCustom();
public static boolean isMineLeave(ConversationInfo conversationInfo) {
ALog.d(LIB_TAG, TAG, "isMineLeave");
if (conversationInfo.getAttachment() instanceof NotificationAttachment) {
NotificationAttachment notify = (NotificationAttachment) conversationInfo.getAttachment();
ALog.d(LIB_TAG, TAG, "isMineLeave,notificationType=" + notify.getType());
if (notify.getType() == NotificationType.DismissTeam) {
return true;
}
return (notify.getType() == NotificationType.KickMember
|| notify.getType() == NotificationType.LeaveTeam)
&& conversationInfo.getTeamInfo() != null
&& !conversationInfo.getTeamInfo().isMyTeam();
}
return false;
}
public static String getConversationText(Context context, ConversationInfo conversationInfo) {
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().conversationCustom != null) {
return ConversationKitClient.getConversationUIConfig()
.conversationCustom
.customContentText(context, conversationInfo);
}
return custom.customContentText(context, conversationInfo);
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.common;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.corekit.im.model.FriendInfo;
import com.netease.yunxin.kit.corekit.im.model.UserInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DataUtils {
public static Map<String, UserInfo> getUserInfoMap(List<UserInfo> data) {
if (data == null || data.size() < 1) {
return null;
}
Map<String, UserInfo> result = new HashMap<>();
for (int index = 0; index < data.size(); index++) {
result.put(data.get(index).getAccount(), data.get(index));
}
return result;
}
public static Map<String, FriendInfo> getFriendInfoMap(List<FriendInfo> data) {
if (data == null || data.size() < 1) {
return null;
}
Map<String, FriendInfo> result = new HashMap<>();
for (int index = 0; index < data.size(); index++) {
result.put(data.get(index).getAccount(), data.get(index));
}
return result;
}
public static Map<String, Team> getTeamInfoMap(List<Team> data) {
if (data == null || data.size() < 1) {
return null;
}
Map<String, Team> result = new HashMap<>();
for (int index = 0; index < data.size(); index++) {
result.put(data.get(index).getId(), data.get(index));
}
return result;
}
}

View File

@@ -0,0 +1,201 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.CHAT_KRY;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_CONTACT_SELECTOR_MAX_COUNT;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_REMOTE_EXTENSION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_REQUEST_SELECTOR_NAME;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_REQUEST_SELECTOR_NAME_ENABLE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_SESSION_ID;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_TEAM_CREATED_TIP;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_TEAM_ID;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_CHAT_SEND_TEAM_TIP_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_FUN_ADD_FRIEND_PAGE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_FUN_CHAT_TEAM_PAGE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_FUN_CONTACT_SELECTOR_PAGE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_FUN_CREATE_ADVANCED_TEAM_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_FUN_CREATE_NORMAL_TEAM_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_TEAM_INVITE_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.REQUEST_CONTACT_SELECTOR_KEY;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.netease.nimlib.sdk.team.model.CreateTeamResult;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.common.ui.photo.TransHelper;
import com.netease.yunxin.kit.common.ui.widgets.ContentListPopView;
import com.netease.yunxin.kit.common.utils.SizeUtils;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.corekit.route.XKitRouter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/** pop menu factory */
public final class FunPopItemFactory {
private static final String TAG = "PopItemFactory";
public static ContentListPopView.Item getAddFriendItem(Context context) {
LinearLayout.LayoutParams params = getParams(context);
return new ContentListPopView.Item.Builder()
.configView(
getView(context, R.string.add_friend, R.drawable.fun_ic_conversation_add_friend))
.configParams(params)
.configClickListener(
v -> XKitRouter.withKey(PATH_FUN_ADD_FRIEND_PAGE).withContext(context).navigate())
.build();
}
public static ContentListPopView.Item getCreateAdvancedTeamItem(
Context context, int memberLimit) {
LinearLayout.LayoutParams params = getParams(context);
int requestCode = 1;
return new ContentListPopView.Item.Builder()
.configView(
getView(
context, R.string.create_advanced_team, R.drawable.fun_ic_conversation_create_team))
.configParams(params)
.configClickListener(
getClickListener(
context, requestCode, PATH_FUN_CREATE_ADVANCED_TEAM_ACTION, memberLimit))
.build();
}
public static ContentListPopView.Item getCreateGroupTeamItem(Context context, int memberLimit) {
LinearLayout.LayoutParams params = getParams(context);
int requestCode = 2;
return new ContentListPopView.Item.Builder()
.configView(
getView(
context, R.string.create_group_team, R.drawable.fun_ic_conversation_create_team))
.configParams(params)
.configClickListener(
getClickListener(context, requestCode, PATH_FUN_CREATE_NORMAL_TEAM_ACTION, memberLimit))
.build();
}
public static ContentListPopView.Item getDivideLineItem(Context context) {
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, SizeUtils.dp2px(0.5f));
params.setMargins(SizeUtils.dp2px(42), 0, 0, 0);
return new ContentListPopView.Item.Builder()
.configView(getDivideView(context))
.configParams(params)
.build();
}
private static View.OnClickListener getClickListener(
Context context, int requestCode, String createMethod, int memberLimit) {
return v ->
TransHelper.launchTask(
context,
requestCode,
(activity, integer) -> {
XKitRouter.withKey(PATH_FUN_CONTACT_SELECTOR_PAGE)
.withParam(KEY_CONTACT_SELECTOR_MAX_COUNT, memberLimit)
.withParam(KEY_REQUEST_SELECTOR_NAME_ENABLE, true)
.withContext(activity)
.withRequestCode(integer)
.navigate();
return null;
},
intentResultInfo -> {
if (intentResultInfo == null
|| !intentResultInfo.getSuccess()
|| intentResultInfo.getValue() == null) {
return null;
}
Intent data = intentResultInfo.getValue();
ArrayList<String> list = data.getStringArrayListExtra(REQUEST_CONTACT_SELECTOR_KEY);
if (list == null || list.isEmpty()) {
ALog.e(TAG, "no one was chosen.");
return null;
}
XKitRouter.withKey(createMethod)
.withParam(
KEY_REQUEST_SELECTOR_NAME,
data.getStringArrayListExtra(KEY_REQUEST_SELECTOR_NAME))
.navigate(
res -> {
if (res.getSuccess() && res.getValue() instanceof CreateTeamResult) {
Team teamInfo = ((CreateTeamResult) res.getValue()).getTeam();
Map<String, Object> map = new HashMap<>(1);
map.put(
KEY_TEAM_CREATED_TIP,
context.getString(R.string.create_advanced_team_success));
// 发送创建成功群里提示信息
XKitRouter.withKey(PATH_CHAT_SEND_TEAM_TIP_ACTION)
.withParam(KEY_SESSION_ID, teamInfo.getId())
.withParam(KEY_REMOTE_EXTENSION, map)
.navigate();
// 邀请加入群,处理邀请通知早于创建成功同志
XKitRouter.withKey(PATH_TEAM_INVITE_ACTION)
.withParam(KEY_TEAM_ID, teamInfo.getId())
.withParam(REQUEST_CONTACT_SELECTOR_KEY, list)
.navigate();
//跳转到会话页面
XKitRouter.withKey(PATH_FUN_CHAT_TEAM_PAGE)
.withContext(context)
.withParam(CHAT_KRY, teamInfo)
.navigate();
} else {
ALog.e(TAG, "create team failed.");
}
});
return null;
});
}
private static View getView(Context context, int txtId, int drawableId) {
TextView textView = new TextView(context);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setTextSize(16);
textView.setMaxLines(1);
textView.setText(txtId);
int marginSize =
(int) context.getResources().getDimension(R.dimen.fun_add_pop_item_margin_right_top);
int padding = (int) context.getResources().getDimension(R.dimen.fun_add_pop_item_padding);
Drawable drawable = ContextCompat.getDrawable(context, drawableId);
if (drawable != null) {
drawable.setBounds(padding, 0, marginSize + padding, marginSize);
textView.setCompoundDrawables(drawable, null, null, null);
}
textView.setPadding(
(int) context.getResources().getDimension(R.dimen.dimen_8_dp),
0,
(int) context.getResources().getDimension(R.dimen.dimen_14_dp),
0);
textView.setMinWidth((int) context.getResources().getDimension(R.dimen.fun_add_pop_item_width));
textView.setTextColor(
context.getResources().getColor(R.color.fun_conversation_add_pop_text_color));
textView.setCompoundDrawablePadding(marginSize);
return textView;
}
private static View getDivideView(Context context) {
View view = new View(context);
view.setBackgroundResource(R.color.color_5a5a5a);
return view;
}
private static LinearLayout.LayoutParams getParams(Context context) {
return new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
(int) context.getResources().getDimension(R.dimen.fun_add_pop_item_height));
}
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.fun.viewholder.FunConversationP2PViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.fun.viewholder.FunConversationTeamViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.interfaces.IConverationSelectorListener;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.corekit.im.utils.RouterConstant;
/**
* conversation view holder factory to create view holder in recyclerview
*/
public class FunViewHolderFactory implements IConversationFactory {
public boolean isSelector;
public boolean isServices = false;
public IConverationSelectorListener listener;
public void setSelector(boolean selector) {
isSelector = selector;
}
public void setServices(boolean services) {
isServices = services;
}
@Override
public ConversationBean CreateBean(ConversationInfo info) {
ConversationBean bean = new ConversationBean(info);
if (info.getSessionType() == SessionTypeEnum.P2P) {
return new ConversationBean(
info,
RouterConstant.PATH_FUN_CHAT_P2P_PAGE,
ConversationConstant.ViewType.CHAT_VIEW,
RouterConstant.CHAT_ID_KRY,
info.getContactId());
} else if (info.getSessionType() == SessionTypeEnum.Team
|| info.getSessionType() == SessionTypeEnum.SUPER_TEAM) {
return new ConversationBean(
info,
RouterConstant.PATH_FUN_CHAT_TEAM_PAGE,
ConversationConstant.ViewType.TEAM_VIEW,
RouterConstant.CHAT_ID_KRY,
info.getContactId());
}
return bean;
}
@Override
public int getItemViewType(ConversationBean data) {
return data.viewType;
}
@Override
public BaseViewHolder<ConversationBean> createViewHolder(
@NonNull ViewGroup parent, int viewType) {
FunConversationViewHolderBinding binding =
FunConversationViewHolderBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
binding.ivSelector.setVisibility(isSelector ? View.VISIBLE : View.GONE);
binding.viewSelectorclick.setVisibility(isSelector ? View.VISIBLE : View.GONE);
if (viewType == ConversationConstant.ViewType.TEAM_VIEW) {
FunConversationTeamViewHolder teamViewHolder = new FunConversationTeamViewHolder(binding);
teamViewHolder.setItemSelectorlistener(listener);
return teamViewHolder;
} else {
FunConversationP2PViewHolder p2PViewHolder = new FunConversationP2PViewHolder(binding);
p2PViewHolder.setIsServices(isServices);
p2PViewHolder.setItemSelectorlistener(listener);
return p2PViewHolder;
}
}
public void setOnSelectorClickListener(IConverationSelectorListener selectorClickListener) {
this.listener = selectorClickListener;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.page;
import android.os.Bundle;
import android.view.LayoutInflater;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import com.netease.yunxin.kit.common.ui.activities.BaseActivity;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationActivityBinding;
public class FunConversationActivity extends BaseActivity {
private FunConversationActivityBinding viewBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
changeStatusBarColor(R.color.fun_conversation_page_bg_color);
viewBinding = FunConversationActivityBinding.inflate(LayoutInflater.from(this));
setContentView(viewBinding.getRoot());
FragmentManager fragmentManager = getSupportFragmentManager();
FunConversationFragment fragment = new FunConversationFragment();
fragmentManager
.beginTransaction()
.add(R.id.conversation_container, fragment)
.commitAllowingStateLoss();
}
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.page;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.common.utils.SizeUtils;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConfig;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationFragmentBinding;
import com.netease.yunxin.kit.conversationkit.ui.fun.FunViewHolderFactory;
import com.netease.yunxin.kit.conversationkit.ui.interfaces.IConverationSelectorListener;
import com.netease.yunxin.kit.conversationkit.ui.page.ConversationBaseFragment;
public class FunConversationFragment extends ConversationBaseFragment {
public static FunConversationFragment getInstance(boolean isselector) {
FunConversationFragment fragment = new FunConversationFragment();
Bundle bundle = new Bundle();
bundle.putBoolean("isSelector", isselector);
fragment.setArguments(bundle);
return fragment;
}
private FunConversationFragmentBinding viewBinding;
private boolean needSelector = false;
public IConverationSelectorListener converationSelectorListener;
@Override
public View initViewAndGetRootView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
viewBinding = FunConversationFragmentBinding.inflate(inflater, container, false);
Bundle bundle = getArguments();
if (bundle != null) {
needSelector = bundle.getBoolean("isSelector");
}
initView();
return viewBinding.getRoot();
}
public void setOnItemSelectorListener(IConverationSelectorListener onItemSelectorListener) {
this.converationSelectorListener = onItemSelectorListener;
}
private void initView() {
conversationView = viewBinding.conversationView;
titleBarView = viewBinding.titleBar;
titleBarView.setVisibility(View.GONE);
viewBinding.searchLayout.setVisibility(View.GONE);
networkErrorView = viewBinding.errorTv;
emptyView = viewBinding.emptyLayout;
FunViewHolderFactory funViewHolderFactory = new FunViewHolderFactory();
funViewHolderFactory.isSelector = needSelector;
funViewHolderFactory.setOnSelectorClickListener(converationSelectorListener);
setViewHolderFactory(funViewHolderFactory);
// viewBinding.conversationView.addItemDecoration(getItemDecoration());
// loadUIConfig();
}
public void refreshView() {
}
public RecyclerView.ItemDecoration getItemDecoration() {
return new RecyclerView.ItemDecoration() {
final int topPadding = SizeUtils.dp2px(0.25f);
final int indent = SizeUtils.dp2px(76);
@Override
public void onDrawOver(
@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int left = parent.getPaddingLeft() + indent;
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getBottom() + params.bottomMargin;
int bottom = top + topPadding;
Paint paint = new Paint();
paint.setColor(
parent.getResources().getColor(R.color.fun_conversation_item_divide_line_color));
canvas.drawRect(left, top, right, bottom, paint);
}
}
};
}
private void loadUIConfig() {
if (ConversationKitClient.getConversationUIConfig() == null) {
return;
}
ConversationUIConfig config = ConversationKitClient.getConversationUIConfig();
viewBinding.titleBar.setLeftImageClick(
v -> {
if (config.titleBarLeftClick != null) {
config.titleBarLeftClick.onClick(v);
}
});
if (config.conversationComparator != null) {
setComparator(config.conversationComparator);
}
if (config.conversationFactory != null) {
setViewHolderFactory(config.conversationFactory);
}
if (!config.showTitleBar) {
titleBarView.setVisibility(View.GONE);
} else {
titleBarView.setVisibility(View.VISIBLE);
titleBarView.setHeadImageVisible(config.showTitleBarLeftIcon ? View.VISIBLE : View.GONE);
titleBarView.showRightImageView(config.showTitleBarRightIcon);
if (config.titleBarTitle != null) {
titleBarView.setTitle(config.titleBarTitle);
}
if (config.titleBarTitleColor != null) {
titleBarView.setTitleColor(config.titleBarTitleColor);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarRightRes != null) {
titleBarView.setRightImageRes(config.titleBarRightRes);
}
}
if (config.customLayout != null) {
config.customLayout.customizeConversationLayout(this);
}
}
public TitleBarView getTitleBar() {
return viewBinding.titleBar;
}
public LinearLayout getTopLayout() {
return viewBinding.topLayout;
}
public LinearLayout getBodyLayout() {
return viewBinding.bodyLayout;
}
public FrameLayout getBottomLayout() {
return viewBinding.bottomLayout;
}
public FrameLayout getBodyTopLayout() {
return viewBinding.bodyTopLayout;
}
public TextView getErrorTextView() {
return viewBinding.errorTv;
}
public void setEmptyViewVisible(int visible) {
viewBinding.emptyLayout.setVisibility(visible);
}
public View getEmptyView() {
return viewBinding.emptyLayout;
}
}

View File

@@ -0,0 +1,194 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.page;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.common.utils.SizeUtils;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConfig;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationFragmentBinding;
import com.netease.yunxin.kit.conversationkit.ui.fun.FunViewHolderFactory;
import com.netease.yunxin.kit.conversationkit.ui.interfaces.IConverationSelectorListener;
import com.netease.yunxin.kit.conversationkit.ui.page.ConversationBaseFragment;
public class FunFriendConversationFragment extends ConversationBaseFragment {
public static FunFriendConversationFragment getInstance(boolean isselector) {
FunFriendConversationFragment fragment = new FunFriendConversationFragment();
Bundle bundle = new Bundle();
fragment.setArguments(bundle);
return fragment;
}
private FunConversationFragmentBinding viewBinding;
private boolean needSelector = false;
public IConverationSelectorListener converationSelectorListener;
@Override
public View initViewAndGetRootView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
viewBinding = FunConversationFragmentBinding.inflate(inflater, container, false);
initView();
return viewBinding.getRoot();
}
public void setOnItemSelectorListener(IConverationSelectorListener onItemSelectorListener) {
this.converationSelectorListener = onItemSelectorListener;
}
private void initView() {
conversationView = viewBinding.conversationView;
titleBarView = viewBinding.titleBar;
titleBarView.setVisibility(View.GONE);
viewBinding.searchLayout.setVisibility(View.GONE);
networkErrorView = viewBinding.errorTv;
emptyView = viewBinding.emptyLayout;
FunViewHolderFactory funViewHolderFactory = new FunViewHolderFactory();
funViewHolderFactory.isSelector = false;
funViewHolderFactory.setOnSelectorClickListener(converationSelectorListener);
setViewHolderFactory(funViewHolderFactory);
// viewBinding.conversationView.addItemDecoration(getItemDecoration());
// loadUIConfig();
}
public void refreshView() {
}
public RecyclerView.ItemDecoration getItemDecoration() {
return new RecyclerView.ItemDecoration() {
final int topPadding = SizeUtils.dp2px(0.25f);
final int indent = SizeUtils.dp2px(76);
@Override
public void onDrawOver(
@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int left = parent.getPaddingLeft() + indent;
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getBottom() + params.bottomMargin;
int bottom = top + topPadding;
Paint paint = new Paint();
paint.setColor(
parent.getResources().getColor(R.color.fun_conversation_item_divide_line_color));
canvas.drawRect(left, top, right, bottom, paint);
}
}
};
}
private void loadUIConfig() {
if (ConversationKitClient.getConversationUIConfig() == null) {
return;
}
ConversationUIConfig config = ConversationKitClient.getConversationUIConfig();
viewBinding.titleBar.setLeftImageClick(
v -> {
if (config.titleBarLeftClick != null) {
config.titleBarLeftClick.onClick(v);
}
});
if (config.conversationComparator != null) {
setComparator(config.conversationComparator);
}
if (config.conversationFactory != null) {
setViewHolderFactory(config.conversationFactory);
}
if (!config.showTitleBar) {
titleBarView.setVisibility(View.GONE);
} else {
titleBarView.setVisibility(View.VISIBLE);
titleBarView.setHeadImageVisible(config.showTitleBarLeftIcon ? View.VISIBLE : View.GONE);
titleBarView.showRightImageView(config.showTitleBarRightIcon);
if (config.titleBarTitle != null) {
titleBarView.setTitle(config.titleBarTitle);
}
if (config.titleBarTitleColor != null) {
titleBarView.setTitleColor(config.titleBarTitleColor);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarRightRes != null) {
titleBarView.setRightImageRes(config.titleBarRightRes);
}
}
if (config.customLayout != null) {
config.customLayout.customizeConversationLayout(this);
}
}
public TitleBarView getTitleBar() {
return viewBinding.titleBar;
}
public LinearLayout getTopLayout() {
return viewBinding.topLayout;
}
public LinearLayout getBodyLayout() {
return viewBinding.bodyLayout;
}
public FrameLayout getBottomLayout() {
return viewBinding.bottomLayout;
}
public FrameLayout getBodyTopLayout() {
return viewBinding.bodyTopLayout;
}
public TextView getErrorTextView() {
return viewBinding.errorTv;
}
public void setEmptyViewVisible(int visible) {
viewBinding.emptyLayout.setVisibility(visible);
}
public View getEmptyView() {
return viewBinding.emptyLayout;
}
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.page;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.common.utils.SizeUtils;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConfig;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationFragmentBinding;
import com.netease.yunxin.kit.conversationkit.ui.fun.FunViewHolderFactory;
import com.netease.yunxin.kit.conversationkit.ui.interfaces.IConverationSelectorListener;
import com.netease.yunxin.kit.conversationkit.ui.page.ConversationBaseFragment;
public class FunGroupConversationFragment extends ConversationBaseFragment {
public static FunGroupConversationFragment getInstance(boolean isselector) {
FunGroupConversationFragment fragment = new FunGroupConversationFragment();
Bundle bundle = new Bundle();
bundle.putBoolean("isSelector", isselector);
fragment.setArguments(bundle);
return fragment;
}
private FunConversationFragmentBinding viewBinding;
private boolean needSelector = false;
public IConverationSelectorListener converationSelectorListener;
@Override
public View initViewAndGetRootView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
viewBinding = FunConversationFragmentBinding.inflate(inflater, container, false);
Bundle bundle = getArguments();
if (bundle != null) {
needSelector = bundle.getBoolean("isSelector");
}
initView();
return viewBinding.getRoot();
}
public void setOnItemSelectorListener(IConverationSelectorListener onItemSelectorListener) {
this.converationSelectorListener = onItemSelectorListener;
}
private void initView() {
conversationView = viewBinding.conversationView;
titleBarView = viewBinding.titleBar;
titleBarView.setVisibility(View.GONE);
viewBinding.searchLayout.setVisibility(View.GONE);
networkErrorView = viewBinding.errorTv;
emptyView = viewBinding.emptyLayout;
FunViewHolderFactory funViewHolderFactory = new FunViewHolderFactory();
funViewHolderFactory.isSelector = needSelector;
funViewHolderFactory.setOnSelectorClickListener(converationSelectorListener);
setViewHolderFactory(funViewHolderFactory);
// viewBinding.conversationView.addItemDecoration(getItemDecoration());
// loadUIConfig();
}
public void refreshView() {
}
public RecyclerView.ItemDecoration getItemDecoration() {
return new RecyclerView.ItemDecoration() {
final int topPadding = SizeUtils.dp2px(0.25f);
final int indent = SizeUtils.dp2px(76);
@Override
public void onDrawOver(
@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int left = parent.getPaddingLeft() + indent;
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getBottom() + params.bottomMargin;
int bottom = top + topPadding;
Paint paint = new Paint();
paint.setColor(
parent.getResources().getColor(R.color.fun_conversation_item_divide_line_color));
canvas.drawRect(left, top, right, bottom, paint);
}
}
};
}
private void loadUIConfig() {
if (ConversationKitClient.getConversationUIConfig() == null) {
return;
}
ConversationUIConfig config = ConversationKitClient.getConversationUIConfig();
viewBinding.titleBar.setLeftImageClick(
v -> {
if (config.titleBarLeftClick != null) {
config.titleBarLeftClick.onClick(v);
}
});
if (config.conversationComparator != null) {
setComparator(config.conversationComparator);
}
if (config.conversationFactory != null) {
setViewHolderFactory(config.conversationFactory);
}
if (!config.showTitleBar) {
titleBarView.setVisibility(View.GONE);
} else {
titleBarView.setVisibility(View.VISIBLE);
titleBarView.setHeadImageVisible(config.showTitleBarLeftIcon ? View.VISIBLE : View.GONE);
titleBarView.showRightImageView(config.showTitleBarRightIcon);
if (config.titleBarTitle != null) {
titleBarView.setTitle(config.titleBarTitle);
}
if (config.titleBarTitleColor != null) {
titleBarView.setTitleColor(config.titleBarTitleColor);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarRightRes != null) {
titleBarView.setRightImageRes(config.titleBarRightRes);
}
}
if (config.customLayout != null) {
config.customLayout.customizeConversationLayout(this);
}
}
public TitleBarView getTitleBar() {
return viewBinding.titleBar;
}
public LinearLayout getTopLayout() {
return viewBinding.topLayout;
}
public LinearLayout getBodyLayout() {
return viewBinding.bodyLayout;
}
public FrameLayout getBottomLayout() {
return viewBinding.bottomLayout;
}
public FrameLayout getBodyTopLayout() {
return viewBinding.bodyTopLayout;
}
public TextView getErrorTextView() {
return viewBinding.errorTv;
}
public void setEmptyViewVisible(int visible) {
viewBinding.emptyLayout.setVisibility(visible);
}
public View getEmptyView() {
return viewBinding.emptyLayout;
}
}

View File

@@ -0,0 +1,233 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.viewholder;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import androidx.collection.LruCache;
import com.netease.yunxin.kit.conversationkit.ui.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
public class EmojiManager {
private static final String EMOJI_DIR = "emoji/";
// max cache size
private static final int CACHE_MAX_SIZE = 1024;
private static Pattern pattern;
// default entries
private static final List<Entry> defaultEntries = new ArrayList<>();
// text to entry
private static final Map<String, Entry> text2entry = new HashMap<>();
// asset bitmap cache, key: asset path
private static LruCache<String, Bitmap> drawableCache;
private static WeakReference<Context> sContext;
public static void init(Context context) {
sContext = new WeakReference<>(context);
load(context);
pattern = makePattern();
drawableCache =
new LruCache<String, Bitmap>(CACHE_MAX_SIZE) {
@Override
protected void entryRemoved(
boolean evicted, @NonNull String key, @NonNull Bitmap oldValue, Bitmap newValue) {
if (oldValue != newValue) oldValue.recycle();
}
};
}
public static Context getContext() {
return sContext.get();
}
private static class Entry {
String text;
String assetPath;
Entry(String text, String assetPath) {
this.text = text;
this.assetPath = assetPath;
}
}
public static int getDisplayCount() {
return defaultEntries.size();
}
public static Drawable getDisplayDrawable(Context context, int index) {
String text =
(index >= 0 && index < defaultEntries.size() ? defaultEntries.get(index).text : null);
return text == null ? null : getDrawable(context, text);
}
public static String getDisplayText(int index) {
return index >= 0 && index < defaultEntries.size() ? defaultEntries.get(index).text : null;
}
public static Pattern getPattern() {
return pattern;
}
public static Drawable getDrawable(Context context, String text) {
Entry entry = text2entry.get(text);
if (entry == null) {
return null;
}
Bitmap cache = drawableCache.get(entry.assetPath);
if (cache == null) {
cache = loadAssetBitmap(context, entry.assetPath);
}
return new BitmapDrawable(context.getResources(), cache);
}
public static String getDrawablePath(Context context, String text) {
Entry entry = text2entry.get(text);
if (entry == null) {
return null;
}
return entry.assetPath;
}
private static Pattern makePattern() {
return Pattern.compile(patternOfDefault());
}
private static String patternOfDefault() {
return "\\[[^\\[]{1,20}\\]";
}
private static Bitmap loadAssetBitmap(Context context, String assetPath) {
InputStream is = null;
try {
Resources resources = context.getResources();
Options options = new Options();
options.inDensity = DisplayMetrics.DENSITY_HIGH;
options.inScreenDensity = resources.getDisplayMetrics().densityDpi;
options.inTargetDensity = resources.getDisplayMetrics().densityDpi;
is = context.getAssets().open(assetPath);
Bitmap bitmap = BitmapFactory.decodeStream(is, new Rect(), options);
if (bitmap != null) {
drawableCache.put(assetPath, bitmap);
}
return bitmap;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private static void load(Context context) {
new EntryLoader().load(context);
}
private static class EntryLoader {
private String catalog = "";
void load(Context context) {
try {
XmlResourceParser xmlParser = context.getResources().getXml(R.xml.emoji);
//
try {
int event = xmlParser.getEventType(); //先获取当前解析器光标在哪
while (event != XmlPullParser.END_DOCUMENT) { //如果还没到文档的结束标志,那么就继续往下处理
if (event == XmlPullParser.START_TAG) { //一般都是获取标签的属性值,所以在这里数据你需要的数据
if (xmlParser.getName().equals("Catalog")) {
catalog = xmlParser.getAttributeValue(0);
} else if (xmlParser.getName().equals("Emoticon")) {
String fileName = xmlParser.getAttributeValue(0);
String tag = xmlParser.getAttributeValue(2);
Entry entry = new Entry(tag, EMOJI_DIR + catalog + "/" + fileName);
if (catalog.equals("default")) {
defaultEntries.add(entry);
if (fileName.startsWith("emoji_"))
text2entry.put(entry.text, entry);
}
if (catalog.equals("taotao")) {
defaultEntries.add(entry);
if (fileName.startsWith("taotao_"))
text2entry.put(entry.text, entry);
}
if (catalog.equals("lele")) {
defaultEntries.add(entry);
if (fileName.startsWith("lele_"))
text2entry.put(entry.text, entry);
}
if (catalog.equals("tingting")) {
defaultEntries.add(entry);
if (fileName.startsWith("tingting_"))
text2entry.put(entry.text, entry);
}
if (catalog.equals("tlt")) {
defaultEntries.add(entry);
if (fileName.startsWith("tlt_"))
text2entry.put(entry.text, entry);
}
}
}
event = xmlParser.next(); //将当前解析器光标往下一步移
}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,238 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.viewholder;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.netease.nim.highavailable.LogUtils;
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.msg.model.IMMessage;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.chatkit.repo.ContactRepo;
import com.netease.yunxin.kit.common.ui.utils.TimeFormatUtils;
import com.netease.yunxin.kit.common.ui.viewholder.BaseBean;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.common.ui.viewholder.ViewHolderClickListener;
import com.netease.yunxin.kit.conversationkit.ui.ConversationCustom;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConfig;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.TimeConversationUtils;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationUtils;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.interfaces.IConverationSelectorListener;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.corekit.im.IMKitClient;
import com.netease.yunxin.kit.corekit.im.custom.CustomAttachment;
import com.netease.yunxin.kit.corekit.im.model.FriendInfo;
import java.util.List;
import java.util.regex.Matcher;
public class FunConversationBaseViewHolder extends BaseViewHolder<ConversationBean> {
protected FunConversationViewHolderBinding viewBinding;
protected Drawable stickTopDrawable;
protected Drawable itemDrawable;
protected Drawable selectorDrawable;
protected Drawable unselectorDrawable;
public IConverationSelectorListener itemSelectorlistener;
public boolean isServices = false;
public void setItemSelectorlistener(IConverationSelectorListener itemSelectorlistener) {
this.itemSelectorlistener = itemSelectorlistener;
}
public FunConversationBaseViewHolder(@NonNull FunConversationViewHolderBinding binding) {
super(binding.getRoot());
viewBinding = binding;
}
public void setIsServices(boolean misServices) {
isServices = misServices;
}
@Override
public void onBindData(ConversationBean data, int position) {
loadUIConfig();
viewBinding.ivSelector.setBackground(data.isSelector ? selectorDrawable : unselectorDrawable);
String conversationContent = ConversationUtils.getConversationText(itemView.getContext(), data.infoData);
if (!TextUtils.isEmpty(conversationContent)) {
if (data.infoData.getSessionType() == SessionTypeEnum.Team && data.infoData.getMsgType() != MsgTypeEnum.notification &&
(!data.infoData.getFromAccount().equals(IMKitClient.account()))) {
//群聊展示 谁谁谁
String contactId = data.infoData.getFromAccount();
String name = data.infoData.getFromNick();
if (ContactRepo.isFriend(contactId)) {
FriendInfo friendInfo = ContactRepo.getFriend(contactId);
if (friendInfo != null && !TextUtils.isEmpty(friendInfo.getAlias())) {
name = friendInfo.getAlias();
}
}
if (conversationContent.equals(viewBinding.getRoot().getContext().getResources().getString(R.string.chat_message_revoke_content))) {
conversationContent = (TextUtils.isEmpty(name) ? "" : name + " ") + conversationContent;
} else {
conversationContent = (TextUtils.isEmpty(name) ? "" : name + ":") + conversationContent;
}
}
}
// viewBinding.messageTv.setText(conversationContent);
ConversationCustom.identifyExpression(
viewBinding.getRoot().getContext(),
viewBinding.messageTv,
conversationContent);
viewBinding.messageTv.setTextColor(viewBinding.getRoot().getContext().getResources().getColor(R.color.fun_conversation_item_sub_title_text_color));
viewBinding.timeTv.setText(
TimeConversationUtils.formatMillisecond(
viewBinding.getRoot().getContext(), data.infoData.getTime()));
if (data.getTextdraft() != null) {
viewBinding.draftTv.setVisibility(View.VISIBLE);
viewBinding.messageTv.setVisibility(View.GONE);
viewBinding.draftTvContent.setVisibility(View.VISIBLE);
ConversationCustom.identifyExpression(
viewBinding.getRoot().getContext(),
viewBinding.draftTvContent,
data.getTextdraft());
} else {
viewBinding.draftTv.setVisibility(View.GONE);
viewBinding.messageTv.setVisibility(View.VISIBLE);
viewBinding.draftTvContent.setVisibility(View.GONE);
}
if (data.infoData.isStickTop()) {
viewBinding.rootLayout.setBackground(stickTopDrawable);
} else {
viewBinding.rootLayout.setBackground(itemDrawable);
}
if (data.infoData.getMute()) {
viewBinding.muteIv.setVisibility(View.VISIBLE);
} else {
viewBinding.muteIv.setVisibility(View.GONE);
}
if (data.infoData.getUnreadCount() > 0) {
int count = data.infoData.getUnreadCount();
String content;
if (count >= 100) {
content = "99+";
} else {
content = String.valueOf(count);
}
if (data.infoData.getMute()) {
viewBinding.unreadTv.setBackgroundResource(R.drawable.bg_conversation_dark_dot);
} else {
viewBinding.unreadTv.setBackgroundResource(R.drawable.bg_conversation_red_dot);
}
viewBinding.unreadTv.setText(content);
viewBinding.unreadTv.setVisibility(View.VISIBLE);
Log.i("INFO", "value==" + viewBinding.messageTv.getText().toString());
if (data.infoData.getAttachment() instanceof CustomAttachment) {
CustomAttachment customAttachment = (CustomAttachment) data.infoData.getAttachment();
if (customAttachment.getType() == 1005 || customAttachment.getType() == 1004 || customAttachment.getType() == 1006) {
viewBinding.messageTv.setTextColor(Color.parseColor("#ff4e54"));
}
}
} else {
viewBinding.unreadTv.setVisibility(View.GONE);
}
// if (data.infoData.getMute()) {
// viewBinding.muteIv.setVisibility(View.VISIBLE);
//// viewBinding.unreadTv.setVisibility(View.GONE);
// } else {
//
//
// }
viewBinding.getRoot().setOnClickListener(v -> itemListener.onClick(v, data, position));
viewBinding.getRoot().setOnLongClickListener(v -> itemListener.onLongClick(v, data, position));
viewBinding.avatarView.setOnClickListener(v -> itemListener.onAvatarClick(v, data, position));
viewBinding.avatarView.setOnLongClickListener(
v -> itemListener.onAvatarLongClick(v, data, position));
viewBinding.viewSelectorclick.setOnClickListener(v -> {
if (itemSelectorlistener != null) data.setSelector(!data.isSelector);
itemSelectorlistener.onSelectorClick(v, data, position);
});
}
private void loadUIConfig() {
itemDrawable =
viewBinding
.getRoot()
.getContext()
.getDrawable(R.drawable.fun_conversation_view_holder_selector);
stickTopDrawable =
viewBinding
.getRoot()
.getContext()
.getDrawable(R.drawable.fun_conversation_view_holder_stick_selector);
selectorDrawable =
viewBinding
.getRoot()
.getContext()
.getDrawable(R.drawable.fun_ic_selected_conversation);
unselectorDrawable =
viewBinding
.getRoot()
.getContext()
.getDrawable(R.drawable.fun_ic_unselector_conversation);
// if (ConversationKitClient.getConversationUIConfig() != null) {
// ConversationUIConfig config = ConversationKitClient.getConversationUIConfig();
// if (config.itemTitleColor != null) {
// viewBinding.nameTv.setTextColor(config.itemTitleColor);
// }
// if (config.itemTitleSize != null) {
// viewBinding.nameTv.setTextSize(config.itemTitleSize);
// }
//
// if (config.itemContentColor != null) {
// viewBinding.messageTv.setTextColor(config.itemContentColor);
// }
// if (config.itemContentSize != null) {
// viewBinding.messageTv.setTextSize(config.itemContentSize);
// }
//
// if (config.itemDateColor != null) {
// viewBinding.timeTv.setTextColor(config.itemDateColor);
// }
// if (config.itemDateSize != null) {
// viewBinding.timeTv.setTextSize(config.itemDateSize);
// }
//
// if (config.avatarCornerRadius != null) {
// viewBinding.avatarView.setCornerRadius(config.avatarCornerRadius);
// }
// if (config.itemBackground != null) {
// itemDrawable = config.itemBackground;
// }
// if (config.itemStickTopBackground != null) {
// stickTopDrawable = config.itemStickTopBackground;
// }
// }
}
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.viewholder;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public class FunConversationP2PViewHolder extends FunConversationBaseViewHolder {
public FunConversationP2PViewHolder(@NonNull FunConversationViewHolderBinding binding) {
super(binding);
}
@Override
public void onBindData(ConversationBean data, int position) {
super.onBindData(data, position);
String name = data.infoData.getName();
String avatName = data.infoData.getAvatarName();
if (TextUtils.isEmpty(data.infoData.getAvatar())) {
viewBinding.avatarView.setData(R.drawable.default_head_img, data.infoData.getAvatarName());
} else {
viewBinding.avatarView.setData(
data.infoData.getAvatar(),
data.infoData.getAvatarName());
}
if (data.isServices) {
viewBinding.tvMyteamtype.setBackgroundResource(R.drawable.cornor_stroke_6877fe_6dp);
viewBinding.tvMyteamtype.setTextColor(viewBinding.getRoot().getContext().getColor(R.color.color_6877fe));
viewBinding.tvMyteamtype.setText(viewBinding.getRoot().getContext().getResources().getString(R.string.services_text));
viewBinding.tvMyteamtype.setVisibility(View.VISIBLE);
} else {
viewBinding.tvMyteamtype.setVisibility(View.GONE);
}
viewBinding.aliasTv.setVisibility(View.GONE);
if (avatName.equals(name)) {
viewBinding.nameTv.setEllipsize(TextUtils.TruncateAt.MIDDLE);
viewBinding.nameTv.setText(name);
viewBinding.nameTv.setMaxWidth(dip2px(viewBinding.nameTv.getContext(), 120));
} else {
viewBinding.nameTv.setMaxWidth(dip2px(viewBinding.nameTv.getContext(), 50));
viewBinding.nameTv.setEllipsize(TextUtils.TruncateAt.END);
viewBinding.aliasTv.setVisibility(View.VISIBLE);
viewBinding.nameTv.setText(avatName);
viewBinding.aliasTv.setText("(" + name + ")");
}
}
public int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.fun.viewholder;
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.common.ui.utils.AvatarColor;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationHelper;
import com.netease.yunxin.kit.conversationkit.ui.databinding.FunConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.corekit.im.IMKitClient;
public class FunConversationTeamViewHolder extends FunConversationBaseViewHolder {
public FunConversationTeamViewHolder(@NonNull FunConversationViewHolderBinding binding) {
super(binding);
}
@SuppressLint("UseCompatLoadingForDrawables")
@Override
public void onBindData(ConversationBean data, int position) {
super.onBindData(data, position);
viewBinding.tvMyteamtype.setVisibility(View.GONE);
viewBinding.aliasTv.setVisibility(View.GONE);
if (data.infoData.getTeamInfo() != null) {
Team teamInfo = data.infoData.getTeamInfo();
if (TextUtils.isEmpty(teamInfo.getIcon())) {
viewBinding.avatarView.setData(R.drawable.ic_group_defaulthead, "head", 1);
} else {
viewBinding.avatarView.setData(
teamInfo.getIcon(), teamInfo.getName(), AvatarColor.avatarColor(teamInfo.getId()));
}
viewBinding.nameTv.setText(teamInfo.getName() + "(" + teamInfo.getMemberCount() + ")");
if (data.infoData.getTeamInfo().getCreator().equals(IMKitClient.account())) {
viewBinding.tvMyteamtype.setBackgroundResource(R.drawable.cornor_6877fe_6dp);
viewBinding.tvMyteamtype.setTextColor(Color.parseColor("#ffffffff"));
viewBinding.tvMyteamtype.setText(viewBinding.getRoot().getContext().getResources().getString(R.string.mine_text));
viewBinding.tvMyteamtype.setVisibility(View.VISIBLE);
}
// viewBinding.groupNumber.setVisibility(View.VISIBLE);
// viewBinding.groupNumber.setText("(" + teamInfo.getMemberCount() + ")");
}
if (data.infoData.getUnreadCount() > 0
&& ConversationHelper.hasAit(data.infoData.getContactId())) {
viewBinding.aitTv.setVisibility(View.VISIBLE);
viewBinding.draftTvContent.setVisibility(View.GONE);
viewBinding.messageTv.setVisibility(View.VISIBLE);
viewBinding.draftTv.setVisibility(View.GONE);
} else {
viewBinding.aitTv.setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,12 @@
package com.netease.yunxin.kit.conversationkit.ui.interfaces;
import android.view.View;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public interface IConverationSelectorListener {
void onSelectorClick(View view, ConversationBean data, int position);
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.interfaces;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.conversationkit.ui.view.ConversationView;
public interface IConversationLayout {
public TitleBarView getTitleBar();
public LinearLayout getTopLayout();
public LinearLayout getBodyLayout();
public ConversationView getConversationView();
public FrameLayout getBottomLayout();
public FrameLayout getBodyTopLayout();
public TextView getErrorTextView();
public void setEmptyViewVisible(int visible);
}

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.model;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.BaseBean;
import java.util.Objects;
public class ConversationBean extends BaseBean {
private static final String TAG = "ConversationBean";
public ConversationInfo infoData;
public String textdraft;
public boolean isSelector;
public boolean isServices;
public boolean isHide;
public boolean isSelector() {
return isSelector;
}
public boolean isHide() {
return isHide;
}
public void setHide(boolean hide) {
isHide = hide;
}
public void setSelector(boolean selector) {
isSelector = selector;
}
public String getTextdraft() {
return textdraft;
}
public void setTextdraft(String textdraft) {
this.textdraft = textdraft;
}
public ConversationBean(ConversationInfo data) {
infoData = data;
}
public ConversationBean(ConversationInfo data, String router, int viewType) {
this(data, router, viewType, null, null);
}
/**
* @param data 会话数据
* @param router 会话跳转的路由地址
* @param viewType 会话展示的View类型
* @param paramKey 跳转需要传递的参数KEY
* @param paramValue 跳转需要传递的参数Value
*/
public ConversationBean(
ConversationInfo data, String router, int viewType, String paramKey, Object paramValue) {
infoData = data;
this.router = router;
this.viewType = viewType;
this.paramKey = paramKey;
this.param = paramValue;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ConversationBean)) return false;
ConversationBean that = (ConversationBean) o;
return Objects.equals(infoData.getContactId(), that.infoData.getContactId())
&& Objects.equals(infoData.getSessionType(), that.infoData.getSessionType());
}
@Override
public int hashCode() {
return Objects.hash(infoData.getContactId(), infoData.getSessionType());
}
}

View File

@@ -0,0 +1,28 @@
package com.netease.yunxin.kit.conversationkit.ui.model;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.corekit.event.BaseEvent;
import java.util.List;
public class DeleteConversionEvent extends BaseEvent {
public static final String EVENT_TYPE = "UpdateConversionEvent";
public List<ConversationBean> conversationBeanList;
@NonNull
@Override
public String getType() {
return EVENT_TYPE;
}
public List<ConversationBean> getConversationBeanList() {
return conversationBeanList;
}
public void setConversationBeanList(List<ConversationBean> conversationBeanList) {
this.conversationBeanList = conversationBeanList;
}
}

View File

@@ -0,0 +1,186 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.CHAT_KRY;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_CONTACT_SELECTOR_MAX_COUNT;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_REMOTE_EXTENSION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_REQUEST_SELECTOR_NAME;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_REQUEST_SELECTOR_NAME_ENABLE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_SESSION_ID;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_TEAM_CREATED_TIP;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.KEY_TEAM_ID;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_ADD_FRIEND_PAGE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_CHAT_SEND_TEAM_TIP_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_CHAT_TEAM_PAGE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_CONTACT_SELECTOR_PAGE;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_CREATE_ADVANCED_TEAM_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_CREATE_NORMAL_TEAM_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.PATH_TEAM_INVITE_ACTION;
import static com.netease.yunxin.kit.corekit.im.utils.RouterConstant.REQUEST_CONTACT_SELECTOR_KEY;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.netease.nimlib.sdk.team.model.CreateTeamResult;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.common.ui.photo.TransHelper;
import com.netease.yunxin.kit.common.ui.widgets.ContentListPopView;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.corekit.route.XKitRouter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/** pop menu factory */
public final class PopItemFactory {
private static final String TAG = "PopItemFactory";
public static ContentListPopView.Item getAddFriendItem(Context context) {
LinearLayout.LayoutParams params = getParams(context);
params.setMargins(
(int) context.getResources().getDimension(R.dimen.pop_text_margin_left),
(int) context.getResources().getDimension(R.dimen.pop_text_margin_right_top),
0,
0);
return new ContentListPopView.Item.Builder()
.configView(getView(context, R.string.add_friend, R.drawable.ic_conversation_add_friend))
.configParams(params)
.configClickListener(
v -> XKitRouter.withKey(PATH_ADD_FRIEND_PAGE).withContext(context).navigate())
.build();
}
public static ContentListPopView.Item getCreateAdvancedTeamItem(
Context context, int memberLimit) {
LinearLayout.LayoutParams params = getParams(context);
params.setMargins(
(int) context.getResources().getDimension(R.dimen.pop_text_margin_left),
0,
0,
(int) context.getResources().getDimension(R.dimen.pop_text_margin_right_top));
int requestCode = 1;
return new ContentListPopView.Item.Builder()
.configView(
getView(
context, R.string.create_advanced_team, R.drawable.ic_conversation_advanced_team))
.configParams(params)
.configClickListener(
getClickListener(context, requestCode, PATH_CREATE_ADVANCED_TEAM_ACTION, memberLimit))
.build();
}
public static ContentListPopView.Item getCreateGroupTeamItem(Context context, int memberLimit) {
LinearLayout.LayoutParams params = getParams(context);
params.setMargins(
(int) context.getResources().getDimension(R.dimen.pop_text_margin_left),
0,
(int) context.getResources().getDimension(R.dimen.pop_text_margin_right_top),
0);
int requestCode = 2;
return new ContentListPopView.Item.Builder()
.configView(
getView(context, R.string.create_group_team, R.drawable.ic_conversation_group_team))
.configParams(params)
.configClickListener(
getClickListener(context, requestCode, PATH_CREATE_NORMAL_TEAM_ACTION, memberLimit))
.build();
}
private static View.OnClickListener getClickListener(
Context context, int requestCode, String createMethod, int memberLimit) {
return v ->
TransHelper.launchTask(
context,
requestCode,
(activity, integer) -> {
XKitRouter.withKey(PATH_CONTACT_SELECTOR_PAGE)
.withParam(KEY_CONTACT_SELECTOR_MAX_COUNT, memberLimit)
.withParam(KEY_REQUEST_SELECTOR_NAME_ENABLE, true)
.withContext(activity)
.withRequestCode(integer)
.navigate();
return null;
},
intentResultInfo -> {
if (intentResultInfo == null
|| !intentResultInfo.getSuccess()
|| intentResultInfo.getValue() == null) {
return null;
}
Intent data = intentResultInfo.getValue();
ArrayList<String> list = data.getStringArrayListExtra(REQUEST_CONTACT_SELECTOR_KEY);
if (list == null || list.isEmpty()) {
ALog.e(TAG, "no one was chosen.");
return null;
}
XKitRouter.withKey(createMethod)
.withParam(
KEY_REQUEST_SELECTOR_NAME,
data.getStringArrayListExtra(KEY_REQUEST_SELECTOR_NAME))
.navigate(
res -> {
if (res.getSuccess() && res.getValue() instanceof CreateTeamResult) {
Team teamInfo = ((CreateTeamResult) res.getValue()).getTeam();
Map<String, Object> map = new HashMap<>(1);
map.put(
KEY_TEAM_CREATED_TIP,
context.getString(R.string.create_advanced_team_success));
// 发送创建成功群里提示信息
XKitRouter.withKey(PATH_CHAT_SEND_TEAM_TIP_ACTION)
.withParam(KEY_SESSION_ID, teamInfo.getId())
.withParam(KEY_REMOTE_EXTENSION, map)
.navigate();
// 邀请加入群,处理邀请通知早于创建成功同志
XKitRouter.withKey(PATH_TEAM_INVITE_ACTION)
.withParam(KEY_TEAM_ID, teamInfo.getId())
.withParam(REQUEST_CONTACT_SELECTOR_KEY, list)
.navigate();
//跳转到会话页面
XKitRouter.withKey(PATH_CHAT_TEAM_PAGE)
.withContext(context)
.withParam(CHAT_KRY, teamInfo)
.navigate();
} else {
ALog.e(TAG, "create team failed.");
}
});
return null;
});
}
private static View getView(Context context, int txtId, int drawableId) {
TextView textView = new TextView(context);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setTextSize(14);
textView.setMaxLines(1);
textView.setText(txtId);
Drawable drawable = ContextCompat.getDrawable(context, drawableId);
if (drawable != null) {
drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight());
textView.setCompoundDrawables(drawable, null, null, null);
}
textView.setTextColor(ContextCompat.getColor(context, R.color.color_333333));
textView.setCompoundDrawablePadding(
(int) context.getResources().getDimension(R.dimen.pop_text_margin_right_top));
return textView;
}
private static LinearLayout.LayoutParams getParams(Context context) {
return new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
(int) context.getResources().getDimension(R.dimen.pop_item_height));
}
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.normal.viewholder.ConversationP2PViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.normal.viewholder.ConversationTeamViewHolder;
import com.netease.yunxin.kit.corekit.im.utils.RouterConstant;
/** conversation view holder factory to create view holder in recyclerview */
public class ViewHolderFactory implements IConversationFactory {
@Override
public ConversationBean CreateBean(ConversationInfo info) {
ConversationBean bean = new ConversationBean(info);
if (info.getSessionType() == SessionTypeEnum.P2P) {
return new ConversationBean(
info,
RouterConstant.PATH_CHAT_P2P_PAGE,
ConversationConstant.ViewType.CHAT_VIEW,
RouterConstant.CHAT_ID_KRY,
info.getContactId());
} else if (info.getSessionType() == SessionTypeEnum.Team
|| info.getSessionType() == SessionTypeEnum.SUPER_TEAM) {
return new ConversationBean(
info,
RouterConstant.PATH_CHAT_TEAM_PAGE,
ConversationConstant.ViewType.TEAM_VIEW,
RouterConstant.CHAT_ID_KRY,
info.getContactId());
}
return bean;
}
@Override
public int getItemViewType(ConversationBean data) {
return data.viewType;
}
@Override
public BaseViewHolder<ConversationBean> createViewHolder(
@NonNull ViewGroup parent, int viewType) {
ConversationViewHolderBinding binding =
ConversationViewHolderBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
if (viewType == ConversationConstant.ViewType.TEAM_VIEW) {
return new ConversationTeamViewHolder(binding);
} else {
return new ConversationP2PViewHolder(binding);
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.page;
import android.os.Bundle;
import android.view.LayoutInflater;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import com.netease.yunxin.kit.common.ui.activities.BaseActivity;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationActivityBinding;
public class ConversationActivity extends BaseActivity {
private ConversationActivityBinding viewBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ConversationActivityBinding.inflate(LayoutInflater.from(this));
setContentView(viewBinding.getRoot());
FragmentManager fragmentManager = getSupportFragmentManager();
ConversationFragment fragment = new ConversationFragment();
fragmentManager
.beginTransaction()
.add(R.id.conversation_container, fragment)
.commitAllowingStateLoss();
}
}

View File

@@ -0,0 +1,178 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.page;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.netease.yunxin.kit.common.ui.widgets.ContentListPopView;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConfig;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConstant;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationFragmentBinding;
import com.netease.yunxin.kit.conversationkit.ui.normal.PopItemFactory;
import com.netease.yunxin.kit.conversationkit.ui.normal.ViewHolderFactory;
import com.netease.yunxin.kit.conversationkit.ui.page.ConversationBaseFragment;
import com.netease.yunxin.kit.corekit.im.utils.RouterConstant;
import com.netease.yunxin.kit.corekit.route.XKitRouter;
/** conversation list fragment show your recent conversation */
public class ConversationFragment extends ConversationBaseFragment {
private final String TAG = "ConversationFragment";
protected ConversationFragmentBinding viewBinding;
@Override
public View initViewAndGetRootView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
viewBinding = ConversationFragmentBinding.inflate(inflater, container, false);
initView();
return viewBinding.getRoot();
}
public void initView() {
conversationView = viewBinding.conversationView;
titleBarView = viewBinding.titleBar;
networkErrorView = viewBinding.errorTv;
emptyView = viewBinding.emptyLayout;
setViewHolderFactory(new ViewHolderFactory());
loadUIConfig();
viewBinding.titleBar.setRightImageClick(
v -> {
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().titleBarRightClick != null) {
ConversationKitClient.getConversationUIConfig().titleBarRightClick.onClick(v);
return;
}
Context context = getContext();
int memberLimit = ConversationUIConstant.MAX_TEAM_MEMBER;
ContentListPopView contentListPopView =
new ContentListPopView.Builder(context)
.addItem(PopItemFactory.getAddFriendItem(context))
.addItem(PopItemFactory.getCreateGroupTeamItem(context, memberLimit))
.addItem(PopItemFactory.getCreateAdvancedTeamItem(context, memberLimit))
.build();
contentListPopView.showAsDropDown(
v, (int) requireContext().getResources().getDimension(R.dimen.pop_margin_right), 0);
});
viewBinding.titleBar.setRight2ImageClick(
v -> {
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().titleBarRight2Click != null) {
ConversationKitClient.getConversationUIConfig().titleBarRight2Click.onClick(v);
return;
}
XKitRouter.withKey(RouterConstant.PATH_GLOBAL_SEARCH_PAGE)
.withContext(requireContext())
.navigate();
});
}
private void loadUIConfig() {
if (ConversationKitClient.getConversationUIConfig() == null) {
return;
}
ConversationUIConfig config = ConversationKitClient.getConversationUIConfig();
viewBinding.titleBar.setLeftImageClick(
v -> {
if (config.titleBarLeftClick != null) {
config.titleBarLeftClick.onClick(v);
}
});
if (config.conversationComparator != null) {
setComparator(config.conversationComparator);
}
if (config.conversationFactory != null) {
setViewHolderFactory(config.conversationFactory);
}
if (titleBarView != null) {
if (!config.showTitleBar) {
titleBarView.setVisibility(View.GONE);
} else {
titleBarView.setVisibility(View.VISIBLE);
titleBarView.setHeadImageVisible(config.showTitleBarLeftIcon ? View.VISIBLE : View.GONE);
titleBarView.showRight2ImageView(config.showTitleBarRight2Icon);
titleBarView.showRightImageView(config.showTitleBarRightIcon);
if (config.titleBarTitle != null) {
titleBarView.setTitle(config.titleBarTitle);
}
if (config.titleBarTitleColor != null) {
titleBarView.setTitleColor(config.titleBarTitleColor);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarLeftRes != null) {
titleBarView.setLeftImageRes(config.titleBarLeftRes);
}
if (config.titleBarRightRes != null) {
titleBarView.setRightImageRes(config.titleBarRightRes);
}
if (config.titleBarRight2Res != null) {
titleBarView.setRight2ImageRes(config.titleBarRight2Res);
}
}
if (config.customLayout != null) {
config.customLayout.customizeConversationLayout(this);
}
}
}
public TitleBarView getTitleBar() {
return viewBinding.titleBar;
}
public LinearLayout getTopLayout() {
return viewBinding.topLayout;
}
public LinearLayout getBodyLayout() {
return viewBinding.bodyLayout;
}
public FrameLayout getBottomLayout() {
return viewBinding.bottomLayout;
}
public FrameLayout getBodyTopLayout() {
return viewBinding.bodyTopLayout;
}
public TextView getErrorTextView() {
return viewBinding.errorTv;
}
public void setEmptyViewVisible(int visible) {
viewBinding.emptyLayout.setVisibility(visible);
}
public View getEmptyView() {
return viewBinding.emptyLayout;
}
}

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.page;
import static com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant.LIB_TAG;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.common.ui.activities.BaseActivity;
import com.netease.yunxin.kit.common.ui.viewholder.BaseBean;
import com.netease.yunxin.kit.common.ui.viewholder.ViewHolderClickListener;
import com.netease.yunxin.kit.common.ui.viewmodel.LoadStatus;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationSelectActivityBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.normal.page.viewmodel.SelectorViewModel;
import com.netease.yunxin.kit.conversationkit.ui.page.interfaces.ILoadListener;
import com.netease.yunxin.kit.corekit.im.utils.RouterConstant;
import java.util.ArrayList;
import java.util.HashSet;
/** conversation select activity to select a conversation */
public class ConversationSelectActivity extends BaseActivity implements ILoadListener {
private final String TAG = "ConversationSelectActivity";
private ConversationSelectActivityBinding viewBinding;
private SelectorViewModel viewModel;
private HashSet<String> contactIdSet = new HashSet<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ALog.d(LIB_TAG, TAG, "onCreate");
viewBinding = ConversationSelectActivityBinding.inflate(LayoutInflater.from(this), null, false);
setContentView(viewBinding.getRoot());
viewBinding.conversationSelectorView.setViewHolderFactory(new SelectorViewHolderFactory());
viewBinding.conversationSelectorTitleBar.setActionTextColor(
getResources().getColor(R.color.color_666666));
viewBinding.conversationSelectorTitleBar.setActionListener(
v -> {
Intent intent = new Intent();
if (contactIdSet.size() > 0) {
intent.putExtra(
RouterConstant.KEY_CONVERSATION_SELECTOR_KEY,
new ArrayList<String>().addAll(contactIdSet));
}
ALog.d(LIB_TAG, TAG, "ActionListener");
setResult(RESULT_OK, intent);
finish();
});
viewBinding.conversationSelectorTitleBar.setLeftActionListener(
v -> {
finish();
});
viewBinding.conversationSelectorView.setItemClickListener(
new ViewHolderClickListener() {
@Override
public boolean onClick(View v, BaseBean data, int isCheck) {
if (data instanceof ConversationBean) {
if (isCheck == 1) {
contactIdSet.add(((ConversationBean) data).infoData.getContactId());
} else {
contactIdSet.remove(((ConversationBean) data).infoData.getContactId());
}
ALog.d(LIB_TAG, TAG, "ItemClickListener,onClick:" + isCheck);
updateTitleBar();
}
return true;
}
@Override
public boolean onLongClick(View v, BaseBean data, int position) {
ALog.d(LIB_TAG, TAG, "ItemClickListener,onLongClick");
return false;
}
});
viewBinding.conversationSelectorView.setLoadMoreListener(this);
viewModel = new ViewModelProvider(this).get(SelectorViewModel.class);
viewModel
.getQueryLiveData()
.observe(
this,
result -> {
if (result.getLoadStatus() == LoadStatus.Success) {
ALog.d(LIB_TAG, TAG, "QueryLiveData,Success");
viewBinding.conversationSelectorView.setData(result.getData());
}
});
viewModel.fetchConversation();
}
private void updateTitleBar() {
if (contactIdSet.size() < 1) {
viewBinding.conversationSelectorTitleBar.setActionText(
getResources().getString(R.string.sure_title));
viewBinding.conversationSelectorTitleBar.setActionTextColor(
getResources().getColor(R.color.color_666666));
} else {
String text =
String.format(getResources().getString(R.string.sure_count_title), contactIdSet.size());
viewBinding.conversationSelectorTitleBar.setActionText(text);
viewBinding.conversationSelectorTitleBar.setActionTextColor(
getResources().getColor(R.color.color_337eff));
}
}
public static void start(Context context) {
if (context != null) {
Intent intent = new Intent(context, ConversationSelectActivity.class);
context.startActivity(intent);
}
}
@Override
public boolean hasMore() {
return viewModel.hasMore();
}
@Override
public void loadMore(Object last) {
if (last instanceof ConversationBean) {
viewModel.loadMore((ConversationBean) last);
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.page;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant;
import com.netease.yunxin.kit.conversationkit.ui.databinding.SelectorViewHolderLayoutBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.normal.ViewHolderFactory;
import com.netease.yunxin.kit.conversationkit.ui.normal.viewholder.SelectorViewHolder;
/** select fragment or activity view holder factory */
class SelectorViewHolderFactory extends ViewHolderFactory {
@Override
public BaseViewHolder<ConversationBean> createViewHolder(
@NonNull ViewGroup parent, int viewType) {
if (viewType == ConversationConstant.ViewType.CHAT_VIEW
|| viewType == ConversationConstant.ViewType.TEAM_VIEW) {
return new SelectorViewHolder(
SelectorViewHolderLayoutBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false));
}
return null;
}
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.page.viewmodel;
import static com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant.LIB_TAG;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.chatkit.repo.ConversationRepo;
import com.netease.yunxin.kit.chatkit.repo.ConversationRepoAll;
import com.netease.yunxin.kit.common.ui.viewmodel.BaseViewModel;
import com.netease.yunxin.kit.common.ui.viewmodel.FetchResult;
import com.netease.yunxin.kit.common.ui.viewmodel.LoadStatus;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.normal.ViewHolderFactory;
import com.netease.yunxin.kit.corekit.im.provider.FetchCallback;
import java.util.ArrayList;
import java.util.List;
/** select conversation view model */
public class SelectorViewModel extends BaseViewModel {
private final String TAG = "SelectorViewModel";
private final MutableLiveData<FetchResult<List<ConversationBean>>> queryLiveData =
new MutableLiveData<>();
private IConversationFactory conversationFactory = new ViewHolderFactory();
private static final int PAGE_LIMIT = 50;
private boolean hasMore = true;
public MutableLiveData<FetchResult<List<ConversationBean>>> getQueryLiveData() {
return queryLiveData;
}
public void fetchConversation() {
queryConversation(null);
}
public void loadMore(ConversationBean data) {
if (data != null && data.infoData != null) {
queryConversation(data.infoData);
}
}
public void setConversationFactory(IConversationFactory factory) {
this.conversationFactory = factory;
}
private void queryConversation(ConversationInfo data) {
ConversationRepoAll.INSTANCE.getSessionList(
data,
PAGE_LIMIT,
new FetchCallback<List<ConversationInfo>>() {
@Override
public void onSuccess(@Nullable List<ConversationInfo> param) {
FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Success);
if (data != null) {
result.setLoadStatus(LoadStatus.Finish);
}
List<ConversationBean> resultData = new ArrayList<>();
for (int index = 0; param != null && index < param.size(); index++) {
resultData.add(conversationFactory.CreateBean(param.get(index)));
ALog.d(
LIB_TAG, TAG, "queryConversation,onSuccess:" + param.get(index).getContactId());
}
hasMore = param != null && param.size() == PAGE_LIMIT;
result.setData(resultData);
queryLiveData.postValue(result);
}
@Override
public void onFailed(int code) {}
@Override
public void onException(@Nullable Throwable exception) {}
});
}
public boolean hasMore() {
return hasMore;
}
}

View File

@@ -0,0 +1,122 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.viewholder;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;
import com.netease.yunxin.kit.common.ui.utils.TimeFormatUtils;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConfig;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.TimeConversationUtils;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationUtils;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public class ConversationBaseViewHolder extends BaseViewHolder<ConversationBean> {
protected ConversationViewHolderBinding viewBinding;
protected Drawable stickTopDrawable;
protected Drawable itemDrawable;
public ConversationBaseViewHolder(@NonNull ConversationViewHolderBinding binding) {
super(binding.getRoot());
viewBinding = binding;
}
@Override
public void onBindData(ConversationBean data, int position) {
loadUIConfig();
if (data.infoData.isStickTop()) {
viewBinding.rootView.setBackground(stickTopDrawable);
} else {
viewBinding.rootView.setBackground(itemDrawable);
}
viewBinding.messageTv.setText(
ConversationUtils.getConversationText(viewBinding.getRoot().getContext(), data.infoData));
viewBinding.timeTv.setText(
TimeConversationUtils.formatMillisecond(
viewBinding.getRoot().getContext(), data.infoData.getTime()));
if (data.infoData.getMute()) {
viewBinding.muteIv.setVisibility(View.VISIBLE);
viewBinding.unreadTv.setVisibility(View.GONE);
} else {
viewBinding.muteIv.setVisibility(View.GONE);
if (data.infoData.getUnreadCount() > 0) {
int count = data.infoData.getUnreadCount();
String content;
if (count >= 100) {
content = "99+";
} else {
content = String.valueOf(count);
}
viewBinding.unreadTv.setText(content);
viewBinding.unreadTv.setVisibility(View.VISIBLE);
} else {
viewBinding.unreadTv.setVisibility(View.GONE);
}
}
viewBinding.contentLayout.setOnClickListener(v -> itemListener.onClick(v, data, position));
// viewBinding.contentLayout.setOnLongClickListener(
// v -> itemListener.onLongClick(v, data, position));
viewBinding.avatarLayout.setOnClickListener(v -> itemListener.onAvatarClick(v, data, position));
viewBinding.avatarLayout.setOnLongClickListener(
v -> itemListener.onAvatarLongClick(v, data, position));
}
private void loadUIConfig() {
itemDrawable =
ResourcesCompat.getDrawable(
viewBinding.getRoot().getContext().getResources(),
R.drawable.conversation_view_holder_selector,
null);
stickTopDrawable =
ResourcesCompat.getDrawable(
viewBinding.getRoot().getContext().getResources(),
R.drawable.conversation_view_holder_stick_selector,
null);
if (ConversationKitClient.getConversationUIConfig() != null) {
ConversationUIConfig config = ConversationKitClient.getConversationUIConfig();
if (config.itemTitleColor != null) {
viewBinding.nameTv.setTextColor(config.itemTitleColor);
}
if (config.itemTitleSize != null) {
viewBinding.nameTv.setTextSize(config.itemTitleSize);
}
if (config.itemContentColor != null) {
viewBinding.messageTv.setTextColor(config.itemContentColor);
}
if (config.itemContentSize != null) {
viewBinding.messageTv.setTextSize(config.itemContentSize);
}
if (config.itemDateColor != null) {
viewBinding.timeTv.setTextColor(config.itemDateColor);
}
if (config.itemDateSize != null) {
viewBinding.timeTv.setTextSize(config.itemDateSize);
}
if (config.avatarCornerRadius != null) {
viewBinding.avatarView.setCornerRadius(config.avatarCornerRadius);
}
if (config.itemBackground != null) {
itemDrawable = config.itemBackground;
}
if (config.itemStickTopBackground != null) {
stickTopDrawable = config.itemStickTopBackground;
}
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.viewholder;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.common.ui.utils.AvatarColor;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public class ConversationP2PViewHolder extends ConversationBaseViewHolder {
public ConversationP2PViewHolder(@NonNull ConversationViewHolderBinding binding) {
super(binding);
}
@Override
public void onBindData(ConversationBean data, int position) {
super.onBindData(data, position);
String name = data.infoData.getName();
viewBinding.avatarView.setData(
data.infoData.getAvatar(),
data.infoData.getAvatarName(),
AvatarColor.avatarColor(data.infoData.getContactId()));
viewBinding.nameTv.setText(name);
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.viewholder;
import android.annotation.SuppressLint;
import android.view.View;
import androidx.annotation.NonNull;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.common.ui.utils.AvatarColor;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationHelper;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationViewHolderBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public class ConversationTeamViewHolder extends ConversationBaseViewHolder {
public ConversationTeamViewHolder(@NonNull ConversationViewHolderBinding binding) {
super(binding);
}
@SuppressLint("UseCompatLoadingForDrawables")
@Override
public void onBindData(ConversationBean data, int position) {
super.onBindData(data, position);
if (data.infoData.getTeamInfo() != null) {
Team teamInfo = data.infoData.getTeamInfo();
viewBinding.avatarView.setData(
teamInfo.getIcon(), teamInfo.getName(), AvatarColor.avatarColor(teamInfo.getId()));
viewBinding.nameTv.setText(teamInfo.getName());
}
if (ConversationHelper.hasAit(data.infoData.getContactId())
&& data.infoData.getUnreadCount() > 0) {
viewBinding.aitTv.setVisibility(View.VISIBLE);
} else {
viewBinding.aitTv.setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.normal.viewholder;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.common.ui.utils.AvatarColor;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.databinding.SelectorViewHolderLayoutBinding;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
public class SelectorViewHolder extends BaseViewHolder<ConversationBean> {
private SelectorViewHolderLayoutBinding viewBinding;
public SelectorViewHolder(@NonNull ViewGroup itemView) {
super(itemView);
}
public SelectorViewHolder(@NonNull SelectorViewHolderLayoutBinding binding) {
this(binding.getRoot());
viewBinding = binding;
}
@Override
public void onBindData(ConversationBean data, int position) {
viewBinding.avatarView.setData(
data.infoData.getAvatar(),
data.infoData.getName(),
AvatarColor.avatarColor(data.infoData.getContactId()));
viewBinding.conversationNameTv.setText(data.infoData.getName());
viewBinding.rootView.setOnClickListener(
v -> {
viewBinding.conversationSelectorCb.setChecked(
!viewBinding.conversationSelectorCb.isChecked());
if (itemListener != null) {
int check = viewBinding.conversationSelectorCb.isChecked() ? 1 : 0;
itemListener.onClick(v, data, check);
}
});
viewBinding.rootView.setOnLongClickListener(
v -> {
if (itemListener != null) {
return itemListener.onLongClick(v, data, position);
}
return false;
});
}
}

View File

@@ -0,0 +1,576 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.page;
import static com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant.LIB_TAG;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.netease.nimlib.sdk.friend.model.MuteListChangedNotify;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.action.ActionItem;
import com.netease.yunxin.kit.common.ui.dialog.ListAlertDialog;
import com.netease.yunxin.kit.common.ui.fragments.BaseFragment;
import com.netease.yunxin.kit.common.ui.viewholder.BaseBean;
import com.netease.yunxin.kit.common.ui.viewholder.ViewHolderClickListener;
import com.netease.yunxin.kit.common.ui.viewmodel.FetchResult;
import com.netease.yunxin.kit.common.ui.viewmodel.LoadStatus;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.common.utils.NetworkUtils;
import com.netease.yunxin.kit.conversationkit.ui.ConversationKitClient;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationHelper;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.page.interfaces.IConversationCallback;
import com.netease.yunxin.kit.conversationkit.ui.page.interfaces.ILoadListener;
import com.netease.yunxin.kit.conversationkit.ui.page.viewmodel.ConversationViewModel;
import com.netease.yunxin.kit.conversationkit.ui.view.ConversationView;
import com.netease.yunxin.kit.corekit.im.model.FriendInfo;
import com.netease.yunxin.kit.corekit.im.model.UserInfo;
import com.netease.yunxin.kit.corekit.route.XKitRouter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* conversation list fragment show your recent conversation
*/
public abstract class ConversationBaseFragment extends BaseFragment implements ILoadListener {
private final String TAG = "ConversationFragment";
public ConversationViewModel viewModel;
private IConversationCallback conversationCallback;
private Observer<FetchResult<List<ConversationBean>>> changeObserver;
private Observer<FetchResult<ConversationBean>> stickObserver;
private Observer<FetchResult<List<UserInfo>>> userInfoObserver;
private Observer<FetchResult<List<FriendInfo>>> friendInfoObserver;
private Observer<FetchResult<List<Team>>> teamInfoObserver;
private Observer<FetchResult<MuteListChangedNotify>> muteObserver;
private Observer<FetchResult<String>> addRemoveStickObserver;
private Observer<FetchResult<List<String>>> aitObserver;
private Observer<FetchResult<Integer>> unreadCountObserver;
protected IConversationFactory conversationFactory;
protected ConversationView conversationView;
protected TitleBarView titleBarView;
protected View networkErrorView;
protected View emptyView;
private long msgUnreadCountTime = 0;
public final long MSG_UNREAD_COUNT_INTERVAL = 1000;
private Handler conversationHandler = new Handler();
public ConversationChangeListener conversationChangeListener;
public void setConversationChangeListener(ConversationChangeListener listener) {
conversationChangeListener = listener;
}
public interface ConversationChangeListener {
void onChange(FetchResult<List<ConversationBean>> result);
}
public abstract View initViewAndGetRootView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState);
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return initViewAndGetRootView(inflater, container, savedInstanceState);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
viewModel.setComparator(conversationComparator);
if (conversationFactory != null) {
viewModel.setConversationFactory(conversationFactory);
}
viewModel
.getQueryLiveData()
.observe(
this.getViewLifecycleOwner(),
result -> {
if (conversationView != null) {
if (result.getLoadStatus() == LoadStatus.Success) {
conversationView.setData(result.getData());
} else if (result.getLoadStatus() == LoadStatus.Finish) {
conversationView.addData(result.getData());
}
if (emptyView != null) {
if (conversationView.getDataSize() > 0) {
emptyView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.VISIBLE);
}
}
}
if (conversationChangeListener != null) {
conversationChangeListener.onChange(result);
}
doCallback();
});
initObserver();
bindView();
if (networkErrorView != null) {
NetworkUtils.registerNetworkStatusChangedListener(networkStateListener);
}
registerObserver();
viewModel.fetchConversation();
}
public void bindView() {
//设置会话排序Comparator默认按照置顶和时间优先级进行排序
if (conversationView != null) {
conversationView.setComparator(conversationComparator);
conversationView.setLoadMoreListener(this);
conversationView.setItemClickListener(
new ViewHolderClickListener() {
@Override
public boolean onClick(View v, BaseBean data, int position) {
boolean result = false;
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().itemClickListener != null
&& data instanceof ConversationBean) {
result =
ConversationKitClient.getConversationUIConfig()
.itemClickListener
.onClick(
ConversationBaseFragment.this.getContext(),
(ConversationBean) data,
position);
}
if (!result && !TextUtils.isEmpty(data.router)) {
XKitRouter.withKey(data.router)
.withParam(data.paramKey, data.param)
.withContext(ConversationBaseFragment.this.requireContext())
.navigate();
}
return true;
}
@Override
public boolean onAvatarClick(View v, BaseBean data, int position) {
boolean result = false;
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().itemClickListener != null
&& data instanceof ConversationBean) {
result =
ConversationKitClient.getConversationUIConfig()
.itemClickListener
.onAvatarClick(
ConversationBaseFragment.this.getContext(),
(ConversationBean) data,
position);
}
if (!result) {
XKitRouter.withKey(data.router)
.withParam(data.paramKey, data.param)
.withContext(ConversationBaseFragment.this.requireContext())
.navigate();
}
return true;
}
@Override
public boolean onLongClick(View v, BaseBean data, int position) {
boolean result = false;
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().itemClickListener != null
&& data instanceof ConversationBean) {
result =
ConversationKitClient.getConversationUIConfig()
.itemClickListener
.onLongClick(
ConversationBaseFragment.this.getContext(),
(ConversationBean) data,
position);
}
if (!result) {
showStickDialog(data);
}
return true;
}
@Override
public boolean onAvatarLongClick(View v, BaseBean data, int position) {
boolean result = false;
if (ConversationKitClient.getConversationUIConfig() != null
&& ConversationKitClient.getConversationUIConfig().itemClickListener != null
&& data instanceof ConversationBean) {
result =
ConversationKitClient.getConversationUIConfig()
.itemClickListener
.onAvatarLongClick(
ConversationBaseFragment.this.getContext(),
(ConversationBean) data,
position);
}
if (!result) {
showStickDialog(data);
}
return true;
}
});
}
}
public void setViewHolderFactory(IConversationFactory factory) {
conversationFactory = factory;
if (viewModel != null) {
viewModel.setConversationFactory(factory);
}
if (conversationView != null) {
conversationView.setViewHolderFactory(factory);
}
}
public void setComparator(Comparator<ConversationInfo> comparator) {
if (comparator != null) {
conversationComparator = comparator;
if (viewModel != null) {
viewModel.setComparator(
ConversationKitClient.getConversationUIConfig().conversationComparator);
}
if (conversationView != null) {
conversationView.setComparator(
ConversationKitClient.getConversationUIConfig().conversationComparator);
}
}
}
private void initObserver() {
changeObserver =
result -> {
if (conversationView != null) {
if (result.getLoadStatus() == LoadStatus.Success) {
ALog.d(LIB_TAG, TAG, "ChangeLiveData, Success");
for (ConversationBean bean : result.getData()) {
if (TextUtils.isEmpty(bean.infoData.getContent())) {
result.getData().remove(bean);
}
}
if (result.getData().size() > 0) {
conversationView.update(result.getData());
}
} else if (result.getLoadStatus() == LoadStatus.Finish
&& result.getType() == FetchResult.FetchType.Remove) {
ALog.d(LIB_TAG, TAG, "DeleteLiveData, Success");
if (result.getData() == null || result.getData().size() < 1) {
conversationView.removeAll();
} else {
conversationView.remove(result.getData());
}
}
if (emptyView != null) {
if (conversationView.getDataSize() > 0) {
emptyView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.VISIBLE);
}
}
}
if (conversationChangeListener != null) {
conversationChangeListener.onChange(result);
}
doCallback();
};
stickObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Success && conversationView != null) {
ALog.d(LIB_TAG, TAG, "StickLiveData, Success");
conversationView.update(result.getData());
}
doCallback();
};
userInfoObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Success && conversationView != null) {
ALog.d(LIB_TAG, TAG, "UserInfoLiveData, Success");
conversationView.updateUserInfo(result.getData());
}
};
friendInfoObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Success && conversationView != null) {
ALog.d(LIB_TAG, TAG, "FriendInfoLiveData, Success");
conversationView.updateFriendInfo(result.getData());
}
};
teamInfoObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Success && conversationView != null) {
ALog.d(LIB_TAG, TAG, "TeamInfoLiveData, Success");
conversationView.updateTeamInfo(result.getData());
}
};
muteObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Success && conversationView != null) {
ALog.d(LIB_TAG, TAG, "MuteInfoLiveData, Success");
conversationView.updateMuteInfo(result.getData());
}
};
aitObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Finish) {
if (result.getType() == FetchResult.FetchType.Add && conversationView != null) {
ALog.d(LIB_TAG, TAG, "AddStickLiveData, Success");
ConversationHelper.updateAitInfo(result.getData(), true);
conversationView.updateAit(result.getData());
} else if (result.getType() == FetchResult.FetchType.Remove
&& conversationView != null) {
ALog.d(LIB_TAG, TAG, "RemoveStickLiveData, Success");
ConversationHelper.updateAitInfo(result.getData(), false);
conversationView.updateAit(result.getData());
}
}
};
addRemoveStickObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Finish) {
if (result.getType() == FetchResult.FetchType.Add && conversationView != null) {
ALog.d(LIB_TAG, TAG, "AddStickLiveData, Success");
conversationView.addStickTop(result.getData());
} else if (result.getType() == FetchResult.FetchType.Remove
&& conversationView != null) {
ALog.d(LIB_TAG, TAG, "RemoveStickLiveData, Success");
conversationView.removeStickTop(result.getData());
}
}
};
unreadCountObserver =
result -> {
if (result.getLoadStatus() == LoadStatus.Success) {
ALog.d(LIB_TAG, TAG, "unreadCount, Success");
if (conversationCallback != null) {
conversationCallback.updateUnreadCount(
result.getData() == null ? 0 : result.getData());
}
}
};
}
@Override
public void onStop() {
super.onStop();
if (conversationView != null) {
conversationView.setShowTag(false);
}
}
@Override
public void onStart() {
super.onStart();
if (conversationView != null) {
conversationView.setShowTag(true);
}
}
private void registerObserver() {
viewModel.getChangeLiveData().observeForever(changeObserver);
viewModel.getStickLiveData().observeForever(stickObserver);
viewModel.getUserInfoLiveData().observeForever(userInfoObserver);
viewModel.getFriendInfoLiveData().observeForever(friendInfoObserver);
viewModel.getTeamInfoLiveData().observeForever(teamInfoObserver);
viewModel.getMuteInfoLiveData().observeForever(muteObserver);
viewModel.getAddRemoveStickLiveData().observeForever(addRemoveStickObserver);
viewModel.getAitLiveData().observeForever(aitObserver);
viewModel.getUnreadCountLiveData().observeForever(unreadCountObserver);
}
private void unregisterObserver() {
viewModel.getChangeLiveData().removeObserver(changeObserver);
viewModel.getStickLiveData().removeObserver(stickObserver);
viewModel.getUserInfoLiveData().removeObserver(userInfoObserver);
viewModel.getFriendInfoLiveData().removeObserver(friendInfoObserver);
viewModel.getTeamInfoLiveData().removeObserver(teamInfoObserver);
viewModel.getMuteInfoLiveData().removeObserver(muteObserver);
viewModel.getAddRemoveStickLiveData().removeObserver(addRemoveStickObserver);
viewModel.getAitLiveData().removeObserver(aitObserver);
viewModel.getUnreadCountLiveData().removeObserver(unreadCountObserver);
}
public void setConversationCallback(IConversationCallback callback) {
this.conversationCallback = callback;
doCallback();
}
public ConversationViewModel getViewModel() {
return viewModel;
}
private void showStickDialog(BaseBean data) {
if (data instanceof ConversationBean) {
ConversationBean dataBean = (ConversationBean) data;
ListAlertDialog alertDialog = new ListAlertDialog();
alertDialog.setContent(generateDialogContent(dataBean.infoData.isStickTop()));
alertDialog.setTitleVisibility(View.GONE);
alertDialog.setDialogWidth(getResources().getDimension(R.dimen.alert_dialog_width));
alertDialog.setItemClickListener(
action -> {
if (TextUtils.equals(action, ConversationConstant.Action.ACTION_DELETE)) {
viewModel.deleteConversation(2, dataBean);
} else if (TextUtils.equals(action, ConversationConstant.Action.ACTION_HIDE)) {
viewModel.deleteConversation(1, dataBean);
// if (dataBean.infoData.isStickTop()) {
// viewModel.removeStick((ConversationBean) data);
// } else {
// viewModel.addStickTop((ConversationBean) data);
// }
}
alertDialog.dismiss();
});
alertDialog.show(getParentFragmentManager());
}
}
private Comparator<ConversationInfo> conversationComparator =
(bean1, bean2) -> {
int result;
if (bean1 == null) {
result = 1;
} else if (bean2 == null) {
result = -1;
} else if (bean1.isStickTop() == bean2.isStickTop()) {
long time = bean1.getTime() - bean2.getTime();
result = time == 0L ? 0 : (time > 0 ? -1 : 1);
} else {
result = bean1.isStickTop() ? -1 : 1;
}
ALog.d(LIB_TAG, TAG, "conversationComparator, result:" + result);
return result;
};
protected List<ActionItem> generateDialogContent(boolean isStick) {
List<ActionItem> contentList = new ArrayList<>();
// ActionItem stick =
// new ActionItem(
// ConversationConstant.Action.ACTION_STICK,
// 0,
// (isStick ? R.string.cancel_stick_title : R.string.stick_title));
ActionItem hide =
new ActionItem(ConversationConstant.Action.ACTION_HIDE, 0, R.string.hide_title);
ActionItem delete =
new ActionItem(ConversationConstant.Action.ACTION_DELETE, 0, R.string.delete_title);
contentList.add(hide);
contentList.add(delete);
return contentList;
}
private void doCallback() {
long currentTime = System.currentTimeMillis();
ALog.d(LIB_TAG, TAG, "doCallback");
if (viewModel != null && currentTime - msgUnreadCountTime > MSG_UNREAD_COUNT_INTERVAL) {
msgUnreadCountTime = currentTime;
conversationHandler.removeCallbacks(msgUnreadCountRunnable);
viewModel.getUnreadCount();
ALog.d(LIB_TAG, TAG, "doCallback:getUnreadCount");
} else if (viewModel != null && conversationHandler != null) {
ALog.d(LIB_TAG, TAG, "doCallback:conversationHandler");
conversationHandler.removeCallbacks(msgUnreadCountRunnable);
conversationHandler.postDelayed(msgUnreadCountRunnable, MSG_UNREAD_COUNT_INTERVAL);
}
}
private Runnable msgUnreadCountRunnable =
new Runnable() {
@Override
public void run() {
if (viewModel != null) {
viewModel.getUnreadCount();
ALog.d(LIB_TAG, TAG, "msgUnreadCountRunnable:getUnreadCount");
}
}
};
@Override
public void onDestroyView() {
super.onDestroyView();
if (networkErrorView != null) {
NetworkUtils.unregisterNetworkStatusChangedListener(networkStateListener);
}
unregisterObserver();
}
private final NetworkUtils.NetworkStateListener networkStateListener =
new NetworkUtils.NetworkStateListener() {
@Override
public void onAvailable(NetworkInfo network) {
if (networkErrorView == null) {
return;
}
networkErrorView.setVisibility(View.GONE);
}
@Override
public void onLost(NetworkInfo network) {
if (networkErrorView == null) {
return;
}
networkErrorView.setVisibility(View.VISIBLE);
}
};
@Override
public boolean hasMore() {
return viewModel.hasMore();
}
@Override
public void loadMore(Object last) {
if (last instanceof ConversationBean) {
viewModel.loadMore((ConversationBean) last);
}
}
public ConversationView getConversationView() {
return conversationView;
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.page;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
/** conversation view holder factory to create view holder in recyclerview */
public class DefaultViewHolderFactory implements IConversationFactory {
@Override
public ConversationBean CreateBean(ConversationInfo info) {
return new ConversationBean(info);
}
@Override
public int getItemViewType(ConversationBean data) {
return data.viewType;
}
@Override
public BaseViewHolder<ConversationBean> createViewHolder(
@NonNull ViewGroup parent, int viewType) {
return new BaseViewHolder<ConversationBean>(new View(parent.getContext())) {
@Override
public void onBindData(ConversationBean data, int position) {}
};
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.page.interfaces;
/** conversation callback to observer the unread count change */
public interface IConversationCallback {
void updateUnreadCount(int count);
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.page.interfaces;
/** load listener */
public interface ILoadListener {
boolean hasMore();
void loadMore(Object last);
}

View File

@@ -0,0 +1,756 @@
/*
* Copyright (c) 2022 NetEase, Inc. All rights reserved.
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*/
package com.netease.yunxin.kit.chatkit.repo
import com.netease.nimlib.sdk.Observer
import com.netease.nimlib.sdk.friend.model.MuteListChangedNotify
import com.netease.nimlib.sdk.msg.constant.DeleteTypeEnum
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum
import com.netease.nimlib.sdk.msg.model.QueryDirectionEnum
import com.netease.nimlib.sdk.msg.model.RecentContact
import com.netease.nimlib.sdk.msg.model.StickTopSessionInfo
import com.netease.nimlib.sdk.team.constant.TeamMessageNotifyTypeEnum
import com.netease.nimlib.sdk.team.model.Team
import com.netease.yunxin.kit.alog.ALog
import com.netease.yunxin.kit.chatkit.model.ConversationInfo
import com.netease.yunxin.kit.chatkit.utils.ChatKitConstant
import com.netease.yunxin.kit.corekit.im.model.EventObserver
import com.netease.yunxin.kit.corekit.im.model.UserInfo
import com.netease.yunxin.kit.corekit.im.provider.FetchCallback
import com.netease.yunxin.kit.corekit.im.provider.FriendObserver
import com.netease.yunxin.kit.corekit.im.provider.FriendObserverProvider
import com.netease.yunxin.kit.corekit.im.provider.FriendProvider
import com.netease.yunxin.kit.corekit.im.provider.MessageObserverProvider
import com.netease.yunxin.kit.corekit.im.provider.MessageProvider
import com.netease.yunxin.kit.corekit.im.provider.TeamObserverProvider
import com.netease.yunxin.kit.corekit.im.provider.TeamProvider
import com.netease.yunxin.kit.corekit.im.provider.UserInfoObserver
import com.netease.yunxin.kit.corekit.im.provider.UserInfoProvider
import com.netease.yunxin.kit.corekit.im.utils.toDispatchInform
import com.netease.yunxin.kit.corekit.im.utils.toInform
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object ConversationRepoAll {
private const val TAG = "ConversationRepoAll"
private const val LIB_TAG = "ConversationKit"
/**
* 查询会话列表,废弃
*/
@JvmStatic
fun getSessionList(limit: Int, callback: FetchCallback<List<ConversationInfo>?>) {
ALog.d(LIB_TAG, TAG, "getSessionList:$limit")
CoroutineScope(Dispatchers.IO).launch {
MessageProvider.queryRecentConversationList(limit).toDispatchInform(callback) {
val conversationList = convertToConversationInfo(it)
clearDismissTeamSession(conversationList)
}
}
}
/**
* 查询所有会话列表把IM本地保存的会话信息全部查询出来。
* @param callback 异步查询查询结果在调用线程回调。查询结果回话类型为P2P则`ConversationInfo`包含`RecentContact
* `和会话对方的`UserInfo`如果会话类型为Team则`ConversationInfo`包含`RecentContact`和`Team`信息
*/
@JvmStatic
fun getAllSessionList(callback: FetchCallback<List<ConversationInfo>?>) {
ALog.d(LIB_TAG, TAG, "getAllSessionList")
CoroutineScope(Dispatchers.IO).launch {
MessageProvider.queryAllRecentConversationList().toDispatchInform(callback) {
val conversationList = convertToConversationInfo(it)
val result = clearDismissTeamSession(conversationList)
sortSessionList(result)
}
}
}
/**
* 分页查询会话列表支持传入Comparator对会话列表进行排序,查询更早的会话列表
* @param anchor 查询下一页会话列表的起始会话
* @param limit 获取本地会话的数量 最大100超过100修正到100
*/
@JvmStatic
fun getSessionList(
anchor: ConversationInfo?,
limit: Int,
comparator: Comparator<ConversationInfo>?,
callback: FetchCallback<List<ConversationInfo>?>
) {
ALog.d(LIB_TAG, TAG, "getContactList:${anchor?.contactId},$limit")
CoroutineScope(Dispatchers.IO).launch {
MessageProvider.queryRecentConversationList(anchor, QueryDirectionEnum.QUERY_OLD, limit)
.toDispatchInform(callback) { infoList ->
val conversationList = convertToConversationInfo(infoList)
val result = clearDismissTeamSession(conversationList)
if (comparator != null) {
result.sortedWith(comparator)
} else {
sortSessionList(result)
}
}
}
}
/**
* 查询会话列表,排序方式按照置顶会话+最近会话时间进行排序
*/
@JvmStatic
fun getSessionList(
anchor: ConversationInfo?,
limit: Int,
callback: FetchCallback<List<ConversationInfo>?>
) {
ALog.d(LIB_TAG, TAG, "getContactList:${anchor?.contactId},$limit")
this.getSessionList(anchor, limit, null, callback)
}
/**
* 最近会话排序,排序规则置顶>最近消息时间
*/
fun sortSessionList(conversationList: List<ConversationInfo>): List<ConversationInfo> {
if (conversationList.size > 1) {
return conversationList.sortedWith(object : Comparator<ConversationInfo> {
override fun compare(info1: ConversationInfo?, info2: ConversationInfo?): Int {
if (info1 == null) {
return 1
}
if (info2 == null) {
return -1
}
return if (info1.isStickTop == info2.isStickTop) {
val time = info1.time - info2.time
if (time == 0L) {
0
} else if (time > 0) {
-1
} else {
1
}
} else {
if (info1.isStickTop) -1 else 1
}
}
})
}
return conversationList
}
/**
* 添加会话置顶
*/
@JvmStatic
fun addStickTop(
sessionId: String,
typeEnum: SessionTypeEnum,
ext: String? = "",
callback: FetchCallback<StickTopSessionInfo>
) {
ALog.d(LIB_TAG, TAG, "addStickTop:$sessionId")
CoroutineScope(Dispatchers.IO).launch {
MessageProvider.addStickTop(sessionId, typeEnum, ext).toDispatchInform(callback) {
it
}
}
}
/**
* 查询会话是否置顶
*/
@JvmStatic
fun isStickTop(
sessionId: String,
typeEnum: SessionTypeEnum
): Boolean {
ALog.d(LIB_TAG, TAG, "isStickTop:$sessionId")
return MessageProvider.isStickSession(sessionId, typeEnum)
}
/**
* 查询会话是否置顶
*/
fun isStickTop(sessionId: String, typeEnum: SessionTypeEnum, callback: FetchCallback<Boolean>) {
CoroutineScope(Dispatchers.IO).launch {
val result = MessageProvider.isStickSession(sessionId, typeEnum)
ALog.d(LIB_TAG, TAG, "isStickTop:$result")
withContext(Dispatchers.Main) {
callback.onSuccess(result)
}
}
}
/**
* 移除置顶
*/
@JvmStatic
fun removeStickTop(
sessionId: String,
typeEnum: SessionTypeEnum,
ext: String? = "",
callback: FetchCallback<Void>?
) {
ALog.d(LIB_TAG, TAG, "removeStickTop:$sessionId")
CoroutineScope(Dispatchers.IO).launch {
MessageProvider.removeStickTop(sessionId, typeEnum, ext).toDispatchInform(callback) {
it
}
}
}
/**
* 删除会话
*/
@JvmStatic
fun deleteSession(
sessionId: String,
typeEnum: SessionTypeEnum,
deleteTypeEnum: DeleteTypeEnum,
sendAck: Boolean,
callback: FetchCallback<Void>?
) {
ALog.d(LIB_TAG, TAG, "deleteSession:$sessionId,${deleteTypeEnum.value}")
CoroutineScope(Dispatchers.IO).launch {
MessageProvider.deleteConversation(sessionId, typeEnum, deleteTypeEnum, sendAck)
.toDispatchInform(callback) {
it
}
}
}
/**
* 获取消息未读数
*/
@JvmStatic
fun getMsgUnreadCount(): Int {
val result = MessageProvider.getMsgUnreadCount()
ALog.d(LIB_TAG, TAG, "getMsgUnreadCount")
return result
}
/**
* 异步获取消息未读数
*/
@JvmStatic
fun getMsgUnreadCountAsync(callback: FetchCallback<Int>?) {
try {
CoroutineScope(Dispatchers.IO).launch {
ALog.d(LIB_TAG, TAG, "getMsgUnreadCount")
val result = MessageProvider.getMsgUnreadCount()
withContext(Dispatchers.Main) {
callback?.onSuccess(result)
}
}
} catch (e: Exception) {
callback?.onException(e)
}
}
/**
* 是否打开消息提醒
*/
@JvmStatic
fun isNotify(sessionId: String, sessionTypeEnum: SessionTypeEnum): Boolean {
var result = false
if (sessionTypeEnum == SessionTypeEnum.P2P) {
result = FriendProvider.isNotify(sessionId)
} else if (sessionTypeEnum == SessionTypeEnum.Team) {
result = !TeamProvider.getTeamById(sessionId).isAllMute
}
ALog.d(ChatKitConstant.LIB_TAG, TAG, "isNotify${sessionId}$result")
return result
}
/**
* 是否打开消息提醒
*/
@JvmStatic
fun isNotify(
sessionId: String,
sessionTypeEnum: SessionTypeEnum,
callback: FetchCallback<Boolean>
) {
CoroutineScope(Dispatchers.IO).launch {
var result = false
if (sessionTypeEnum == SessionTypeEnum.P2P) {
result = FriendProvider.isNotify(sessionId)
} else if (sessionTypeEnum == SessionTypeEnum.Team) {
result = !TeamProvider.getTeamById(sessionId).isAllMute
}
ALog.d(ChatKitConstant.LIB_TAG, TAG, "isNotify${sessionId}$result")
CoroutineScope(Dispatchers.Main).launch {
callback.onSuccess(result)
}
}
}
/**
* 设置群消息不提醒
*/
@Deprecated(message = "user setNotify() to replace")
@JvmStatic
fun setTeamNotify(teamId: String, mute: Boolean, callback: FetchCallback<Void>?) {
ALog.d(LIB_TAG, TAG, "setTeamNotify:$teamId,$mute")
CoroutineScope(Dispatchers.IO).launch {
TeamProvider.muteTeam(
teamId,
if (mute) TeamMessageNotifyTypeEnum.Mute else TeamMessageNotifyTypeEnum.All
).toDispatchInform(callback) { it }
}
}
/**
* 设置账号的消息提醒状态
*/
@JvmStatic
fun setNotify(
accId: String,
sessionTypeEnum: SessionTypeEnum,
notify: Boolean,
callback: FetchCallback<Void>
) {
ALog.d(ChatKitConstant.LIB_TAG, TAG, "setNotify${accId}$notify")
if (sessionTypeEnum == SessionTypeEnum.P2P) {
CoroutineScope(Dispatchers.IO).launch {
FriendProvider.setNotify(accId, notify).toDispatchInform(callback) {
it
}
}
} else if (sessionTypeEnum == SessionTypeEnum.Team) {
CoroutineScope(Dispatchers.IO).launch {
TeamProvider.muteTeam(
accId,
if (notify) TeamMessageNotifyTypeEnum.All else TeamMessageNotifyTypeEnum.Mute
).toDispatchInform(callback) { it }
}
}
}
/**
* 通知置顶变化
*/
@JvmStatic
fun notifyStickTop(
accId: String,
sessionTypeEnum: SessionTypeEnum
) {
ALog.d(LIB_TAG, TAG, "notifyStickTop:$accId")
CoroutineScope(Dispatchers.Main).launch {
MessageProvider.notifyConversationStick(accId, sessionTypeEnum)
}
}
/**
* 清除所有未读消息
*/
@JvmStatic
fun clearAllUnreadCount() {
ALog.d(LIB_TAG, TAG, "clearAllUnreadCount")
MessageProvider.clearAllUnreadCount()
}
/**
* 会话过滤,过滤掉已经删除的会话
*/
fun clearDismissTeamSession(conversationList: List<ConversationInfo>): List<ConversationInfo> {
ALog.d(LIB_TAG, TAG, "filterSession:${conversationList?.size}")
val result = arrayListOf<ConversationInfo>()
conversationList.map { info ->
// if (info.sessionType == SessionTypeEnum.Team &&
// (info.teamInfo == null || info.teamInfo?.isMyTeam == false)
// ) {
// deleteSession(
// info.contactId,
// info.sessionType,
// DeleteTypeEnum.LOCAL_AND_REMOTE,
// true,
// null
// )
// } else {
// result.add(info)
// }
result.add(info)
}
return result
}
/**
* 补充会话信息,会话列表中的个人信息和群组信息都在这里填充
*/
@JvmStatic
suspend fun convertToConversationInfo(infoList: List<RecentContact>?): List<ConversationInfo> {
ALog.d(LIB_TAG, TAG, "fillSessionInfo:${infoList?.size}")
val teamList = mutableListOf<ConversationInfo>()
val p2pList = mutableListOf<ConversationInfo>()
val conversationList = mutableListOf<ConversationInfo>()
val muteP2pList = FriendProvider.getMuteList()?.toHashSet()
infoList?.map { info ->
val conversationInfo = ConversationInfo(info)
when (info.sessionType) {
SessionTypeEnum.P2P -> {
if (muteP2pList?.contains(conversationInfo.contactId) == true) {
conversationInfo.mute = true
}
conversationList.add(conversationInfo)
p2pList.add(conversationInfo)
}
SessionTypeEnum.Team -> {
conversationList.add(conversationInfo)
teamList.add(conversationInfo)
}
else -> {}
}
conversationInfo.isStickTop =
MessageProvider.isStickSession(info.contactId, info.sessionType)
}
if (p2pList.size > 0) {
fillUserInfo(p2pList)
}
if (teamList.size > 0) {
fillTeamInfo(teamList)
}
return conversationList
}
/**
* 会话信息中,补充用户信息
*/
@JvmStatic
suspend fun fillUserInfo(conversationList: List<ConversationInfo>) {
val conversationMap = mutableMapOf<String, ConversationInfo>()
val userSet = mutableSetOf<String>()
val friendInfoMap = FriendProvider.getFriendList().associateBy {
it.account
}
conversationList.map { info ->
info.friendInfo = friendInfoMap[info.contactId]
val user = UserInfoProvider.getUserInfo(info.contactId)
if (user != null) {
info.userInfo = user
} else {
userSet.add(info.contactId)
conversationMap[info.contactId] = info
}
}
if (userSet.isNotEmpty()) {
UserInfoProvider.fetchUserInfoCoroutine(userSet.toList())
.toInform(object : FetchCallback<List<UserInfo>> {
override fun onSuccess(param: List<UserInfo>?) {
param?.map {
conversationMap[it.account]?.userInfo = it
}
conversationMap.map {
if (it.value.userInfo == null) {
it.value.userInfo = UserInfo(it.key, it.key, null)
}
}
}
override fun onFailed(code: Int) {
ALog.d(LIB_TAG, TAG, "fillUserInfo,fetchUserInfoCoroutine,onFailed:$code")
}
override fun onException(exception: Throwable?) {
ALog.d(LIB_TAG, TAG, "fillUserInfo,fetchUserInfoCoroutine:onException")
}
}) { it }
}
}
/**
* 会话信息中,补充群组信息
*/
@JvmStatic
suspend fun fillTeamInfo(conversationList: List<ConversationInfo>) {
ALog.d(LIB_TAG, TAG, "fillTeamInfo:${conversationList?.size}")
val conversationMap = mutableMapOf<String, ConversationInfo>()
val teamSet = mutableSetOf<String>()
for (info in conversationList) {
teamSet.add(info.contactId)
conversationMap[info.contactId] = info
}
TeamProvider.queryTeamListById(teamSet.toList())
.toInform(object : FetchCallback<List<Team>> {
override fun onSuccess(param: List<Team>?) {
param?.map { team ->
conversationMap[team.id]?.teamInfo = team
conversationMap[team.id]?.mute =
(team.messageNotifyType == TeamMessageNotifyTypeEnum.Mute)
}
ALog.d(
LIB_TAG,
TAG,
"fillTeamConversationInfo,queryTeamListById,onSuccess:${param?.size}"
)
}
override fun onFailed(code: Int) {
ALog.d(
LIB_TAG,
TAG,
"fillTeamConversationInfo,queryTeamListById,onFailed:$code"
)
}
override fun onException(exception: Throwable?) {
ALog.d(LIB_TAG, TAG, "fillTeamConversationInfo,queryTeamListById,onException")
}
}) { it }
}
/**
* 注册会话变化监听器
*/
@Deprecated(
message = "use ConversationObserverRepo.registerSessionChangedObserver() to replace"
)
@JvmStatic
fun registerSessionChangedObserver(
observer: EventObserver<List<ConversationInfo>>
) {
MessageObserverProvider.registerConversationChange(
observer.getObserverInnerSuspend(
convertCoroutineContext = Dispatchers.IO,
convert = { itemList ->
convertToConversationInfo(itemList)
}
),
true
)
}
/**
* 取消会话变化监听器
*/
@Deprecated(
message = "use ConversationObserverRepo.unregisterSessionChangedObserver() to replace"
)
@JvmStatic
fun unregisterSessionChangedObserver(
observer: EventObserver<List<ConversationInfo>>
) {
MessageObserverProvider.registerConversationChange(
observer.getObserverInner(),
false
)
}
/**
* 注册会话删除监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerSessionDeleteObserver() to replace")
@JvmStatic
fun registerSessionDeleteObserver(
observer: EventObserver<ConversationInfo>
) {
MessageObserverProvider.registerConversationDelete(
observer.getObserverInnerSuspend {
it?.let { convertToConversationInfo(arrayListOf(it))[0] }
},
true
)
}
/**
* 取消会话删除监听器
*/
@Deprecated(
message = "use ConversationObserverRepo.unregisterSessionDeleteObserver() to replace"
)
@JvmStatic
fun unregisterSessionDeleteObserver(
observer: EventObserver<ConversationInfo>
) {
MessageObserverProvider.registerConversationDelete(
observer.getObserverInner(),
false
)
}
/**
* 注册用户信息变化监听器
*/
@Deprecated(message = "use ContactObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerUserInfoObserver(observer: UserInfoObserver) {
UserInfoProvider.registerUserInfoObserver(observer, true)
}
/**
* 取消用户信息变化监听器
*/
@Deprecated(message = "use ContactObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterUserInfoObserver(
observer: UserInfoObserver
) {
UserInfoProvider.registerUserInfoObserver(observer, false)
}
/**
* 注册好友信息变化监听器
*/
@Deprecated(message = "use ContactObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerFriendObserver(
observer: FriendObserver
) {
FriendProvider.registerFriendObserver(observer)
}
/**
* 取消好友信息变化监听器
*/
@Deprecated(message = "use ContactObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterFriendObserver(
observer: FriendObserver
) {
FriendProvider.unregisterFriendObserver(observer)
}
/**
* 注册群组移除变化监听器
*/
@Deprecated(message = "use TeamObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerTeamRemoveObserver(
observer: Observer<Team>
) {
TeamObserverProvider.registerTeamRemoveObserver(observer, true)
}
/**
* 取消群组移除变化监听器
*/
@Deprecated(message = "use TeamObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterTeamRemoveObserver(
observer: Observer<Team>
) {
TeamObserverProvider.registerTeamRemoveObserver(observer, false)
}
/**
* 注册群组信息变化监听器
*/
@Deprecated(message = "use TeamObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerTeamUpdateObserver(observer: Observer<List<Team>>) {
TeamObserverProvider.registerTeamUpdateObserver(observer, true)
}
/**
* 取消群组信息变化监听器
*/
@Deprecated(message = "use TeamObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterTeamUpdateObserver(observer: Observer<List<Team>>) {
TeamObserverProvider.registerTeamUpdateObserver(observer, false)
}
/**
* 注册好友消息不提醒变化监听器
*/
@Deprecated(message = "use ContactObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerFriendMuteObserver(observer: Observer<MuteListChangedNotify>) {
FriendObserverProvider.registerMuteChangedObserver(observer, true)
}
/**
* 取消好友消息不提醒变化监听器
*/
@Deprecated(message = "use ContactObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterFriendMuteObserver(observer: Observer<MuteListChangedNotify>) {
FriendObserverProvider.registerMuteChangedObserver(observer, false)
}
/**
* 注册会话置顶变化监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerAddStickTopObserver(observer: Observer<StickTopSessionInfo>) {
MessageObserverProvider.registerConversationAddStick(observer, true)
}
/**
* 取消会话置顶变化监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterAddStickTopObserver(observer: Observer<StickTopSessionInfo>) {
MessageObserverProvider.registerConversationAddStick(observer, false)
}
/**
* 注册置顶移除变化监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerRemoveStickTopObserver(observer: Observer<StickTopSessionInfo>) {
MessageObserverProvider.registerConversationRemoveStick(observer, true)
}
/**
* 取消置顶移除变化监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterRemoveStickTopObserver(observer: Observer<StickTopSessionInfo>) {
MessageObserverProvider.registerConversationRemoveStick(observer, false)
}
/**
* 注册置顶会话变化监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerUpdateStickTopObserver(observer: Observer<StickTopSessionInfo>) {
MessageObserverProvider.registerConversationUpdateStick(observer, true)
}
/**
* 取消置顶会话变化监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterUpdateStickTopObserver(observer: Observer<StickTopSessionInfo>) {
MessageObserverProvider.registerConversationUpdateStick(observer, false)
}
/**
* 注册置顶会话同步监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun registerSyncStickTopObserver(
observer: Observer<List<StickTopSessionInfo>>
) {
MessageObserverProvider.registerConversationSyncStick(observer, true)
}
/**
* 取消置顶会话同步监听器
*/
@Deprecated(message = "use ConversationObserverRepo.registerUserInfoObserver() to replace")
@JvmStatic
fun unregisterSyncStickTopObserver(
observer: Observer<List<StickTopSessionInfo>>
) {
MessageObserverProvider.registerConversationSyncStick(observer, false)
}
}

View File

@@ -0,0 +1,672 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.page.viewmodel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import com.netease.nim.highavailable.LogUtils;
import com.netease.nimlib.sdk.NIMClient;
import com.netease.nimlib.sdk.Observer;
import com.netease.nimlib.sdk.RequestCallbackWrapper;
import com.netease.nimlib.sdk.friend.model.MuteListChangedNotify;
import com.netease.nimlib.sdk.msg.MsgService;
import com.netease.nimlib.sdk.msg.constant.DeleteTypeEnum;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.msg.model.RecentContact;
import com.netease.nimlib.sdk.msg.model.StickTopSessionInfo;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.chatkit.repo.ContactObserverRepo;
import com.netease.yunxin.kit.chatkit.repo.ContactRepo;
import com.netease.yunxin.kit.chatkit.repo.ConversationObserverRepo;
import com.netease.yunxin.kit.chatkit.repo.ConversationRepo;
import com.netease.yunxin.kit.chatkit.repo.ConversationRepoAll;
import com.netease.yunxin.kit.chatkit.repo.TeamObserverRepo;
import com.netease.yunxin.kit.common.ui.utils.ToastX;
import com.netease.yunxin.kit.common.ui.viewmodel.BaseViewModel;
import com.netease.yunxin.kit.common.ui.viewmodel.FetchResult;
import com.netease.yunxin.kit.common.ui.viewmodel.LoadStatus;
import com.netease.yunxin.kit.conversationkit.ui.ConversationUIConstant;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.common.ConversationUtils;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.model.DeleteConversionEvent;
import com.netease.yunxin.kit.conversationkit.ui.page.DefaultViewHolderFactory;
import com.netease.yunxin.kit.corekit.event.EventCenter;
import com.netease.yunxin.kit.corekit.event.EventNotify;
import com.netease.yunxin.kit.corekit.im.IMKitClient;
import com.netease.yunxin.kit.corekit.im.custom.AitEvent;
import com.netease.yunxin.kit.corekit.im.custom.AitInfo;
import com.netease.yunxin.kit.corekit.im.model.EventObserver;
import com.netease.yunxin.kit.corekit.im.model.FriendInfo;
import com.netease.yunxin.kit.corekit.im.model.UserInfo;
import com.netease.yunxin.kit.corekit.im.provider.FetchCallback;
import com.netease.yunxin.kit.corekit.im.provider.FriendChangeType;
import com.netease.yunxin.kit.corekit.im.provider.FriendObserver;
import com.netease.yunxin.kit.corekit.im.provider.FriendProvider;
import com.netease.yunxin.kit.corekit.im.provider.TeamProvider;
import com.netease.yunxin.kit.corekit.im.provider.UserInfoObserver;
import com.netease.yunxin.kit.corekit.im.provider.UserInfoProvider;
import com.netease.yunxin.kit.corekit.im.utils.RouterConstant;
import com.netease.yunxin.kit.corekit.route.XKitRouter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* conversation view model to fetch data or operate conversation
*/
public class ConversationViewModel extends BaseViewModel {
private final String TAG = "ConversationViewModel";
private final String LIB_TAG = "ConversationKit-UI";
private final MutableLiveData<FetchResult<Integer>> unreadCountLiveData = new MutableLiveData<>();
private final MutableLiveData<FetchResult<List<ConversationBean>>> queryLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<List<ConversationBean>>> changeLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<ConversationBean>> stickLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<String>> addRemoveStickLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<List<UserInfo>>> userInfoLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<List<FriendInfo>>> friendInfoLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<List<Team>>> teamInfoLiveData = new MutableLiveData<>();
private final MutableLiveData<FetchResult<MuteListChangedNotify>> muteInfoLiveData =
new MutableLiveData<>();
private final MutableLiveData<FetchResult<List<String>>> aitLiveData = new MutableLiveData<>();
private Comparator<ConversationInfo> comparator;
private IConversationFactory conversationFactory = new DefaultViewHolderFactory();
private static final int PAGE_LIMIT = 50;
private boolean hasMore = true;
private boolean hasStart = false;
public ConversationViewModel() {
//register observer
ConversationObserverRepo.registerSessionChangedObserver(changeObserver);
ConversationObserverRepo.registerSessionDeleteObserver(deleteObserver);
ContactObserverRepo.registerUserInfoObserver(userInfoObserver);
ContactObserverRepo.registerFriendObserver(friendObserver);
TeamObserverRepo.registerTeamUpdateObserver(teamUpdateObserver);
ConversationObserverRepo.registerNotifyChangeObserver(muteObserver);
ConversationObserverRepo.registerAddStickTopObserver(addStickObserver);
ConversationObserverRepo.registerRemoveStickTopObserver(removeStickObserver);
ConversationObserverRepo.registerSyncStickTopObserver(syncStickObserver);
EventNotify<AitEvent> aitNotify =
new EventNotify<AitEvent>() {
@Override
public void onNotify(@NonNull AitEvent message) {
AitEvent aitEvent = (AitEvent) message;
FetchResult<List<String>> result = new FetchResult<>(LoadStatus.Finish);
ALog.d(LIB_TAG, TAG, "aitNotifyonSuccess:" + aitEvent.getEventType().name());
if (aitEvent.getEventType() == AitEvent.AitEventType.Arrive
|| aitEvent.getEventType() == AitEvent.AitEventType.Load) {
result.setFetchType(FetchResult.FetchType.Add);
} else {
result.setFetchType(FetchResult.FetchType.Remove);
}
List<AitInfo> aitInfoList = aitEvent.getAitInfoList();
List<String> sessionIdList = new ArrayList<>();
for (AitInfo info : aitInfoList) {
sessionIdList.add(info.getSessionId());
}
result.setData(sessionIdList);
aitLiveData.setValue(result);
}
@NonNull
@Override
public String getEventType() {
return "AitEvent";
}
};
EventCenter.registerEventNotify(aitNotify);
}
/**
* comparator to sort conversation list
*/
public void setComparator(Comparator<ConversationInfo> comparator) {
this.comparator = comparator;
}
/**
* comparator to sort conversation list
*/
public void setConversationFactory(IConversationFactory factory) {
this.conversationFactory = factory;
}
/**
* query unread count live data
*/
public MutableLiveData<FetchResult<Integer>> getUnreadCountLiveData() {
return unreadCountLiveData;
}
/**
* query conversation live data
*/
public MutableLiveData<FetchResult<List<ConversationBean>>> getQueryLiveData() {
return queryLiveData;
}
/**
* conversation change live data
*/
public MutableLiveData<FetchResult<List<ConversationBean>>> getChangeLiveData() {
return changeLiveData;
}
/**
* conversation stick live data
*/
public MutableLiveData<FetchResult<ConversationBean>> getStickLiveData() {
return stickLiveData;
}
/**
* conversation remove stick live data
*/
public MutableLiveData<FetchResult<String>> getAddRemoveStickLiveData() {
return addRemoveStickLiveData;
}
/**
* userinfo changed live data
*/
public MutableLiveData<FetchResult<List<UserInfo>>> getUserInfoLiveData() {
return userInfoLiveData;
}
/**
* friend changed live data
*/
public MutableLiveData<FetchResult<List<FriendInfo>>> getFriendInfoLiveData() {
return friendInfoLiveData;
}
/**
* team changed live data
*/
public MutableLiveData<FetchResult<List<Team>>> getTeamInfoLiveData() {
return teamInfoLiveData;
}
/**
* mute changed live data
*/
public MutableLiveData<FetchResult<MuteListChangedNotify>> getMuteInfoLiveData() {
return muteInfoLiveData;
}
/**
* conversation @ info
*/
public MutableLiveData<FetchResult<List<String>>> getAitLiveData() {
return aitLiveData;
}
public void getUnreadCount() {
ConversationRepo.getMsgUnreadCountAsync(
new FetchCallback<Integer>() {
@Override
public void onSuccess(@Nullable Integer param) {
FetchResult<Integer> fetchResult = new FetchResult<>(LoadStatus.Success);
fetchResult.setData(param);
unreadCountLiveData.setValue(fetchResult);
}
@Override
public void onFailed(int code) {
ALog.d(LIB_TAG, TAG, "getUnreadCount,onFailed" + code);
ToastX.showShortToast(String.valueOf(code));
}
@Override
public void onException(@Nullable Throwable exception) {
ALog.d(LIB_TAG, TAG, "getUnreadCount,onException");
}
});
}
public void cleanUnreadCount() {
ConversationRepo.clearAllUnreadCount();
}
public void fetchConversation() {
XKitRouter.withKey(RouterConstant.PATH_CHAT_AIT_NOTIFY_ACTION).navigate();
queryConversation(null);
}
public void loadMore(ConversationBean data) {
if (data != null && data.infoData != null) {
ALog.d(LIB_TAG, TAG, "loadMore:" + data.infoData.getContactId());
queryConversation(data.infoData);
}
}
private void queryConversation(ConversationInfo data) {
ALog.d(LIB_TAG, TAG, "queryConversation:" + (data == null));
if (hasStart) {
ALog.d(LIB_TAG, TAG, "queryConversation,has Started return");
return;
}
hasStart = true;
// NIMClient.getService(MsgService.class).queryRecentContacts()
// .setCallback(new RequestCallbackWrapper<List<RecentContact>>() {
// @Override
// public void onResult(int code, List<RecentContact> recents, Throwable e) {
// // recents参数即为最近联系人列表最近会话列表
// if (code == 200) {
// FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Success);
// if (recents != null) {
// result.setLoadStatus(LoadStatus.Finish);
// List<ConversationBean> resultData = new ArrayList<>();
//
// for (int index = 0; recents != null && index < recents.size(); index++) {
// ConversationInfo conversationInfo = new ConversationInfo(recents.get(index));
// if (recents.get(index).getSessionType() == SessionTypeEnum.Team) {
// fillTeamInfo(conversationInfo);
// }
// if (recents.get(index).getSessionType() == SessionTypeEnum.P2P) {
// fillUserInfo(conversationInfo);
// }
// resultData.add(conversationFactory.CreateBean(conversationInfo));
// }
// hasMore = false; //param != null && param.size() == PAGE_LIMIT;
// result.setData(resultData);
// queryLiveData.setValue(result);
// hasStart = false;
// }
// } else {
// ToastX.showShortToast(String.valueOf(code));
// hasStart = false;
// }
// }
// });
ConversationRepoAll.getAllSessionList(
// data,
// PAGE_LIMIT,
// comparator,
new FetchCallback<List<ConversationInfo>>() {
@Override
public void onSuccess(@Nullable List<ConversationInfo> param) {
FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Success);
if (data != null) {
result.setLoadStatus(LoadStatus.Finish);
}
ALog.d(
LIB_TAG,
TAG,
"queryConversation:onSuccess,size=" + (param != null ? param.size() : 0));
List<ConversationBean> resultData = new ArrayList<>();
for (int index = 0; param != null && index < param.size(); index++) {
resultData.add(conversationFactory.CreateBean(param.get(index)));
}
hasMore = false; //param != null && param.size() == PAGE_LIMIT;
result.setData(resultData);
queryLiveData.setValue(result);
hasStart = false;
}
@Override
public void onFailed(int code) {
ALog.d(LIB_TAG, TAG, "queryConversation,onFailed" + code);
ToastX.showShortToast(String.valueOf(code));
hasStart = false;
}
@Override
public void onException(@Nullable Throwable exception) {
ALog.d(LIB_TAG, TAG, "queryConversation,onException");
hasStart = false;
}
});
}
public void deleteMoreConversation(int type, List<ConversationBean> data) {
for (ConversationBean bean : data) {
deleteConversation(type, bean);
}
}
public void fillTeamInfo(ConversationInfo info) {
Team team = TeamProvider.INSTANCE.getTeamById(info.getContactId());
info.setTeamInfo(team);
}
public void fillUserInfo(ConversationInfo info) {
FriendInfo friendInfo = FriendProvider.INSTANCE.getFriendInfo(info.getContactId());
UserInfo userInfo = UserInfoProvider.getUserInfo(info.getContactId(), true);
friendInfo.setUserInfo(userInfo);
info.setFriendInfo(friendInfo);
info.setUserInfo(userInfo);
}
// public void deleteFriendConversation(String accid,ConversationBean conversationBean) {
// RecentContact recentContact = NIMClient.getService(MsgService.class).queryRecentContact(contactId, SessionTypeEnum.P2P);
// ConversationRepo.deleteSession(
// accid,
// SessionTypeEnum.P2P,
// DeleteTypeEnum.LOCAL_AND_REMOTE,
// true,
// new FetchCallback<Void>() {
// @Override
// public void onSuccess(@Nullable Void param) {
// FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Finish);
// result.setFetchType(FetchResult.FetchType.Remove);
// List<ConversationBean> beanList = new ArrayList<>();
// beanList.add(conversationBean);
// result.setData(beanList);
// changeLiveData.setValue(result); //删除更新
//// DeleteConversionEvent updateConversionEvent = new DeleteConversionEvent();
//// updateConversionEvent.setConversationBeanList(beanList);
//// EventCenter.notifyEvent(updateConversionEvent);
// }
//
// @Override
// public void onFailed(int code) {
// ALog.d(LIB_TAG, TAG, "deleteConversation,onFailed:" + code);
// ToastX.showShortToast(String.valueOf(code));
// }
//
// @Override
// public void onException(@Nullable Throwable exception) {
// ALog.d(LIB_TAG, TAG, "deleteConversation,onException");
// }
// });
//
// }
public void deleteConversation(int type, ConversationBean data) {
ALog.d(LIB_TAG, TAG, "deleteConversation,data:" + data.infoData.getContactId());
ConversationRepo.deleteSession(
data.infoData.getContactId(),
data.infoData.getSessionType(),
type == 1 ? DeleteTypeEnum.LOCAL : DeleteTypeEnum.LOCAL_AND_REMOTE,
true,
new FetchCallback<Void>() {
@Override
public void onSuccess(@Nullable Void param) {
ALog.d(LIB_TAG, TAG, "deleteConversation,onSuccess:" + data.infoData.getContactId());
FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Finish);
result.setFetchType(FetchResult.FetchType.Remove);
List<ConversationBean> beanList = new ArrayList<>();
// if (type == 1) {
// data.setHide(true);
// }
beanList.add(data);
result.setData(beanList);
changeLiveData.setValue(result); //删除更新
if (type == 2) {
NIMClient.getService(MsgService.class).clearServerHistory(data.infoData.getContactId(), data.infoData.getSessionType(), false);
}
// DeleteConversionEvent updateConversionEvent = new DeleteConversionEvent();
// updateConversionEvent.setConversationBeanList(beanList);
// EventCenter.notifyEvent(updateConversionEvent);
}
@Override
public void onFailed(int code) {
ALog.d(LIB_TAG, TAG, "deleteConversation,onFailed:" + code);
ToastX.showShortToast(String.valueOf(code));
}
@Override
public void onException(@Nullable Throwable exception) {
ALog.d(LIB_TAG, TAG, "deleteConversation,onException");
}
});
}
public void addStickTop(ConversationBean data) {
ConversationRepoAll.addStickTop(
data.infoData.getContactId(),
data.infoData.getSessionType(),
"",
new FetchCallback<StickTopSessionInfo>() {
@Override
public void onSuccess(@Nullable StickTopSessionInfo param) {
if (param != null) {
FetchResult<ConversationBean> result = new FetchResult<>(LoadStatus.Success);
data.infoData.setStickTop(true);
result.setData(data);
ALog.d(LIB_TAG, TAG, "addStickTop,onSuccess:" + param.getSessionId());
stickLiveData.setValue(result);
}
}
@Override
public void onFailed(int code) {
ALog.d(LIB_TAG, TAG, "addStickTop,onFailed:" + code);
if (code == ConversationUIConstant.ERROR_CODE_NETWORK) {
ToastX.showShortToast(R.string.conversation_network_error_tip);
} else {
ToastX.showShortToast(String.valueOf(code));
}
}
@Override
public void onException(@Nullable Throwable exception) {
ALog.d(LIB_TAG, TAG, "addStickTop,onException");
}
});
}
public void removeStick(ConversationBean data) {
ConversationRepoAll.removeStickTop(
data.infoData.getContactId(),
data.infoData.getSessionType(),
"",
new FetchCallback<Void>() {
@Override
public void onSuccess(@Nullable Void param) {
FetchResult<ConversationBean> result = new FetchResult<>(LoadStatus.Success);
data.infoData.setStickTop(false);
result.setData(data);
ALog.d(LIB_TAG, TAG, "removeStick,onSuccess:" + data.infoData.getContactId());
stickLiveData.setValue(result);
}
@Override
public void onFailed(int code) {
ALog.d(LIB_TAG, TAG, "addStickTop,onFailed:" + code);
if (code == ConversationUIConstant.ERROR_CODE_NETWORK) {
ToastX.showShortToast(R.string.conversation_network_error_tip);
} else {
ToastX.showShortToast(String.valueOf(code));
}
}
@Override
public void onException(@Nullable Throwable exception) {
}
});
}
private final Observer<StickTopSessionInfo> addStickObserver =
param -> {
ALog.d(LIB_TAG, TAG, "addStickObserveronSuccess:" + param.getSessionId());
FetchResult<String> result = new FetchResult<>(LoadStatus.Finish);
result.setFetchType(FetchResult.FetchType.Add);
result.setData(param.getSessionId());
addRemoveStickLiveData.setValue(result);
};
private final Observer<List<StickTopSessionInfo>> syncStickObserver =
param -> {
ALog.d(LIB_TAG, TAG, "syncStickObserver,onSuccess:" + param.size());
queryConversation(null);
};
private final Observer<StickTopSessionInfo> removeStickObserver =
param -> {
ALog.d(LIB_TAG, TAG, "removeStickObserver,onSuccess:" + param.getSessionId());
FetchResult<String> result = new FetchResult<>(LoadStatus.Finish);
result.setFetchType(FetchResult.FetchType.Remove);
result.setData(param.getSessionId());
addRemoveStickLiveData.setValue(result);
};
private final EventObserver<List<ConversationInfo>> changeObserver =
new EventObserver<List<ConversationInfo>>() {
@Override
public void onEvent(@Nullable List<ConversationInfo> param) {
if (param != null) {
FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Success);
List<ConversationBean> resultData = new ArrayList<>();
for (int index = 0; index < param.size(); index++) {
ConversationInfo conversationInfo = param.get(index);
if (ConversationUtils.isMineLeave(conversationInfo)) { //如果是群解散,被移除,被解散不删会话
if (conversationInfo.getFromAccount().equals(IMKitClient.account())) {
deleteConversation(2, conversationFactory.CreateBean(param.get(index)));
} else { //更新未读数
ConversationBean bean = conversationFactory.CreateBean(param.get(index));
resultData.add(bean);
}
// deleteConversation(2, conversationFactory.CreateBean(param.get(index)));
// ALog.d(
// LIB_TAG,
// TAG,
// "changeObserver,DismissTeam,onSuccess:" + param.get(index).getContactId());
// continue;
} else {
ConversationBean bean = conversationFactory.CreateBean(param.get(index));
// bean.param = conversationInfo.getContactId();
// bean.paramKey = RouterConstant.CHAT_ID_KRY;
// if (conversationInfo.getSessionType() == SessionTypeEnum.Team) {
// bean.router = RouterConstant.PATH_CHAT_TEAM_PAGE;
// bean.viewType = 2;
// } else {
// bean.router = RouterConstant.PATH_CHAT_P2P_PAGE;
// bean.viewType = 1;
// }
resultData.add(bean);
}
}
result.setData(resultData);
changeLiveData.setValue(result);
}
}
};
private final EventObserver<ConversationInfo> deleteObserver =
new EventObserver<ConversationInfo>() {
@Override
public void onEvent(@Nullable ConversationInfo param) {
ALog.d(LIB_TAG, TAG, "deleteObserver,onSuccess:" + (param == null));
FetchResult<List<ConversationBean>> result = new FetchResult<>(LoadStatus.Finish);
result.setFetchType(FetchResult.FetchType.Remove);
List<ConversationBean> beanList = new ArrayList<>();
if (param != null) {
beanList.add(conversationFactory.CreateBean(param));
}
result.setData(beanList);
changeLiveData.setValue(result);
}
};
private final UserInfoObserver userInfoObserver =
userList -> {
ALog.d(LIB_TAG, TAG, "userInfoObserver,userList:" + userList.size());
FetchResult<List<UserInfo>> result = new FetchResult<>(LoadStatus.Success);
result.setData(userList);
userInfoLiveData.setValue(result);
};
private final FriendObserver friendObserver =
(friendChangeType, accountList) -> {
ALog.d(LIB_TAG, TAG, "friendObserver,userList:" + accountList.size());
if (friendChangeType == FriendChangeType.Update) {
ContactRepo.getFriendList(
accountList,
new FetchCallback<List<FriendInfo>>() {
@Override
public void onSuccess(@Nullable List<FriendInfo> param) {
ALog.d(
LIB_TAG, TAG, "friendObserver,getFriendInfo,onSuccess:" + accountList.size());
FetchResult<List<FriendInfo>> result = new FetchResult<>(LoadStatus.Success);
result.setData(param);
friendInfoLiveData.setValue(result);
}
@Override
public void onFailed(int code) {
ALog.d(LIB_TAG, TAG, "friendObserver,getFriendInfo,onFailed:" + code);
}
@Override
public void onException(@Nullable Throwable exception) {
ALog.d(
LIB_TAG,
TAG,
"friendObserver,getFriendInfo,onException"
+ (exception != null ? exception.getMessage() : "null"));
}
});
} else if (friendChangeType == FriendChangeType.Delete) {
FetchResult<List<FriendInfo>> result = new FetchResult<>(LoadStatus.Success);
List<FriendInfo> friendList = new ArrayList<>();
for (String account : accountList) {
friendList.add(new FriendInfo(account, null, null));
}
result.setData(friendList);
friendInfoLiveData.setValue(result);
}
};
private final Observer<List<Team>> teamUpdateObserver =
teamList -> {
if (teamList != null) {
ALog.d(LIB_TAG, TAG, "teamUpdateObserver,teamInfoList:" + teamList.size());
FetchResult<List<Team>> result = new FetchResult<>(LoadStatus.Success);
result.setData(teamList);
teamInfoLiveData.setValue(result);
}
};
private final Observer<MuteListChangedNotify> muteObserver =
muteNotify -> {
if (muteNotify != null) {
ALog.d(LIB_TAG, TAG, "muteObserver,muteNotify:" + muteNotify.getAccount());
FetchResult<MuteListChangedNotify> result = new FetchResult<>(LoadStatus.Success);
result.setData(muteNotify);
muteInfoLiveData.setValue(result);
}
};
public boolean hasMore() {
return hasMore;
}
@Override
protected void onCleared() {
super.onCleared();
ConversationObserverRepo.unregisterSessionDeleteObserver(deleteObserver);
ConversationObserverRepo.unregisterSessionChangedObserver(changeObserver);
ContactObserverRepo.unregisterUserInfoObserver(userInfoObserver);
ContactObserverRepo.unregisterFriendObserver(friendObserver);
TeamObserverRepo.unregisterTeamUpdateObserver(teamUpdateObserver);
ConversationObserverRepo.unregisterNotifyChangeObserver(muteObserver);
ConversationObserverRepo.unregisterAddStickTopObserver(addStickObserver);
ConversationObserverRepo.unregisterSyncStickTopObserver(syncStickObserver);
ConversationObserverRepo.unregisterRemoveStickTopObserver(removeStickObserver);
}
}

View File

@@ -0,0 +1,418 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.view;
import static com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant.LIB_TAG;
import android.text.TextUtils;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.netease.nimlib.sdk.friend.model.MuteListChangedNotify;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.team.constant.TeamMessageNotifyTypeEnum;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.BaseViewHolder;
import com.netease.yunxin.kit.common.ui.viewholder.ViewHolderClickListener;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.common.DataUtils;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.page.DefaultViewHolderFactory;
import com.netease.yunxin.kit.corekit.im.IMKitClient;
import com.netease.yunxin.kit.corekit.im.model.FriendInfo;
import com.netease.yunxin.kit.corekit.im.model.UserInfo;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* conversation adapter
*/
public class ConversationAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private final String TAG = "ConversationAdapter";
private IConversationFactory viewHolderFactory = new DefaultViewHolderFactory();
private final List<ConversationBean> conversationList = new ArrayList<>();
private Comparator<ConversationInfo> dataComparator;
private ViewHolderClickListener clickListener;
private boolean isShow = true;
private final LinearLayoutManager layoutManager;
private boolean isShowAll = true;
public ConversationAdapter(LinearLayoutManager layoutManager) {
this.layoutManager = layoutManager;
}
public void setShowAll(boolean showAll) {
this.isShowAll = showAll;
notifyDataSetChanged();
}
/**
* set data and clear conversationList
*/
public void setData(List<ConversationBean> data) {
conversationList.clear();
if (data != null) {
conversationList.addAll(data);
notifyDataSetChanged();
}
}
public void setShowTag(boolean show) {
isShow = show;
if (show) {
notifyDataSetChanged();
}
}
/**
* add data to list forward
*/
public void addForwardData(List<ConversationBean> data) {
if (data != null) {
conversationList.addAll(0, data);
}
}
/**
* append data to list
*/
public void appendData(List<ConversationBean> data) {
if (data != null) {
int position = conversationList.size();
conversationList.addAll(data);
notifyItemInserted(position);
}
}
public void update(List<ConversationBean> data) {
for (int i = 0; data != null && i < data.size(); i++) {
update(data.get(i));
}
}
public void update(ConversationBean data) {
ALog.d(LIB_TAG, TAG, "update" + data.infoData.getContactId());
int position = layoutManager.findFirstVisibleItemPosition();
int removeIndex = -1;
for (int j = 0; j < conversationList.size(); j++) {
if (data.equals(conversationList.get(j))) {
removeIndex = j;
break;
}
}
ALog.d(LIB_TAG, TAG, "update, removeIndex:" + removeIndex);
if (removeIndex > -1) {
conversationList.remove(removeIndex);
int insertIndex = searchComparatorIndex(data);
ALog.d(
LIB_TAG,
TAG,
"update, insertIndex:" + insertIndex + "unread:" + data.infoData.getUnreadCount());
conversationList.add(insertIndex, data);
if (isShow && isShowAll) {
notifyItemMoved(removeIndex, insertIndex);
notifyItemChanged(insertIndex);
} else {
notifyDataSetChanged();
}
} else {
int insertIndex = searchComparatorIndex(data);
conversationList.add(insertIndex, data);
if (isShow && isShowAll) {
notifyItemInserted(insertIndex);
} else {
notifyItemChanged(insertIndex);
}
}
layoutManager.scrollToPosition(position);
}
public void updateSelector(ConversationBean data) {
int size = conversationList.indexOf(data);
if (size >= 0) {
conversationList.set(size, data);
notifyItemChanged(size);
}
}
public void updateUserInfo(List<UserInfo> data) {
Map<String, UserInfo> accountMap = DataUtils.getUserInfoMap(data);
if (accountMap != null) {
for (int i = 0; i < conversationList.size(); i++) {
UserInfo info = conversationList.get(i).infoData.getUserInfo();
if (info != null && accountMap.containsKey(info.getAccount())) {
conversationList.get(i).infoData.setUserInfo(accountMap.get(info.getAccount()));
if (isShow) {
notifyItemChanged(i);
}
}
}
}
}
public void updateFriendInfo(List<FriendInfo> data) {
Map<String, FriendInfo> accountMap = DataUtils.getFriendInfoMap(data);
if (accountMap != null) {
for (int i = 0; i < conversationList.size(); i++) {
UserInfo info = conversationList.get(i).infoData.getUserInfo();
if (info != null && accountMap.containsKey(info.getAccount())) {
conversationList.get(i).infoData.setFriendInfo(accountMap.get(info.getAccount()));
if (isShow) {
notifyItemChanged(i);
}
}
}
}
}
public void updateTeamInfo(List<Team> data) {
Map<String, Team> accountMap = DataUtils.getTeamInfoMap(data);
if (accountMap != null) {
for (int i = 0; i < conversationList.size(); i++) {
ConversationInfo info = conversationList.get(i).infoData;
if (info != null
&& info.getTeamInfo() != null
&& accountMap.containsKey(info.getTeamInfo().getId())) {
Team team = accountMap.get(info.getTeamInfo().getId());
info.setTeamInfo(team);
if (team != null && team.getMessageNotifyType() != null) {
info.setMute(team.getMessageNotifyType() == TeamMessageNotifyTypeEnum.Mute);
}
if (isShow) {
notifyItemChanged(i);
}
}
}
}
}
public void updateTeamInfoType(List<Team> data, boolean isMy) {
Map<String, Team> accountMap = DataUtils.getTeamInfoMap(data);
if (accountMap != null) {
for (int i = 0; i < conversationList.size(); i++) {
ConversationInfo info = conversationList.get(i).infoData;
if (info != null
&& info.getTeamInfo() != null
&& accountMap.containsKey(info.getTeamInfo().getId())) {
Team team = accountMap.get(info.getTeamInfo().getId());
info.setTeamInfo(team);
if (team != null && team.getMessageNotifyType() != null) {
info.setMute(team.getMessageNotifyType() == TeamMessageNotifyTypeEnum.Mute);
}
if (isMy && !team.getCreator().equals(IMKitClient.account())) {
conversationList.remove(i);
notifyItemRemoved(i);
break;
}
if (!isMy && team.getCreator().equals(IMKitClient.account())) {
conversationList.remove(i);
notifyItemRemoved(i);
break;
}
if (isShow) {
notifyItemChanged(i);
}
}
}
}
}
public void updateMuteInfo(MuteListChangedNotify data) {
if (data != null) {
for (int i = 0; i < conversationList.size(); i++) {
String contactId = conversationList.get(i).infoData.getContactId();
if (TextUtils.equals(contactId, data.getAccount())) {
conversationList.get(i).infoData.setMute(data.isMute());
if (isShow) {
notifyItemChanged(i);
}
}
}
}
}
private int searchComparatorIndex(ConversationBean data) {
int index = conversationList.size();
// add stick must be insert 0
if (data.infoData.isStickTop()) {
return 0;
}
for (int i = 0; i < conversationList.size(); i++) {
if (dataComparator != null
&& dataComparator.compare(data.infoData, conversationList.get(i).infoData) < 1) {
index = i;
break;
}
}
return index;
}
public void removeData(List<ConversationBean> dataList) {
if (dataList == null || dataList.size() < 1) {
return;
}
for (ConversationBean data : dataList) {
int index = -1;
for (int j = 0; j < conversationList.size(); j++) {
if (data.equals(conversationList.get(j))) {
index = j;
break;
}
}
if (index > -1) {
removeData(index);
}
}
}
public void removeAll() {
conversationList.clear();
notifyDataSetChanged();
}
public void removeData(String id) {
int index = -1;
for (int j = 0; j < conversationList.size(); j++) {
if (TextUtils.equals(conversationList.get(j).infoData.getContactId(), id)) {
index = j;
break;
}
}
if (index > -1) {
removeData(index);
}
}
public void removeData(int position) {
if (position >= 0 && position < conversationList.size()) {
conversationList.remove(position);
if (isShow) {
notifyItemRemoved(position);
}
}
}
public void updateAit(List<String> idList) {
int index = -1;
for (String id : idList) {
for (int j = 0; j < conversationList.size(); j++) {
if (TextUtils.equals(conversationList.get(j).infoData.getContactId(), id)) {
index = j;
break;
}
}
if (index > -1) {
notifyItemChanged(index);
}
}
}
public void addStickTop(String id) {
int index = -1;
for (int j = 0; j < conversationList.size(); j++) {
if (TextUtils.equals(conversationList.get(j).infoData.getContactId(), id)) {
index = j;
break;
}
}
if (index > -1) {
conversationList.get(index).infoData.setStickTop(true);
ConversationBean data = conversationList.remove(index);
conversationList.add(0, data);
if (isShow) {
notifyItemMoved(index, 0);
notifyItemChanged(0);
}
}
}
public void removeStickTop(String id) {
int index = -1;
for (int j = 0; j < conversationList.size(); j++) {
if (TextUtils.equals(conversationList.get(j).infoData.getContactId(), id)) {
index = j;
break;
}
}
if (index > -1) {
ConversationBean data = conversationList.remove(index);
data.infoData.setStickTop(false);
int insertIndex = searchComparatorIndex(data);
conversationList.add(insertIndex, data);
if (isShow) {
notifyItemMoved(index, insertIndex);
notifyItemChanged(insertIndex);
}
}
}
public void setViewHolderFactory(IConversationFactory factory) {
this.viewHolderFactory = factory;
}
public void setViewHolderClickListener(ViewHolderClickListener listener) {
this.clickListener = listener;
}
public void setComparator(Comparator<ConversationInfo> comparator) {
this.dataComparator = comparator;
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
BaseViewHolder viewHolder = null;
if (viewHolderFactory != null) {
viewHolder = viewHolderFactory.createViewHolder(parent, viewType);
}
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
holder.onBindData(conversationList.get(position), position);
holder.setItemOnClickListener(clickListener);
}
@Override
public int getItemViewType(int position) {
return viewHolderFactory.getItemViewType(conversationList.get(position));
}
@Override
public int getItemCount() {
if (isShowAll) {
return conversationList.size();
} else {
return Math.min(conversationList.size(), 3);
}
// return conversationList.size();
}
public ConversationBean getData(int index) {
if (index >= 0 && index < conversationList.size()) {
return conversationList.get(index);
}
return null;
}
public List<ConversationBean> getConversationList() {
return conversationList;
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.netease.yunxin.kit.common.ui.widgets.TitleBarView;
import com.netease.yunxin.kit.conversationkit.ui.databinding.ConversationViewLayoutBinding;
public class ConversationLayout extends LinearLayout {
private ConversationViewLayoutBinding viewBinding;
public ConversationLayout(Context context) {
super(context);
init(null);
}
public ConversationLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public ConversationLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
public ConversationLayout(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrs) {
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
viewBinding = ConversationViewLayoutBinding.inflate(layoutInflater, this);
}
public TitleBarView getTitleBar() {
return viewBinding.conversationTitleBar;
}
public LinearLayout getTopLayout() {
return viewBinding.conversationTopLayout;
}
public LinearLayout getBodyLayout() {
return viewBinding.conversationBodyLayout;
}
public ConversationView getConversationView() {
return viewBinding.conversationView;
}
public FrameLayout getBottomLayout() {
return viewBinding.conversationBottomLayout;
}
public FrameLayout getBodyTopLayout() {
return viewBinding.conversationBodyTopLayout;
}
public TextView getErrorTextView() {
return viewBinding.conversationNetworkErrorTv;
}
public void setEmptyViewVisible(int visible) {
viewBinding.conversationEmptyView.setVisibility(visible);
}
}

View File

@@ -0,0 +1,251 @@
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
// Use of this source code is governed by a MIT license that can be
// found in the LICENSE file.
package com.netease.yunxin.kit.conversationkit.ui.view;
import static com.netease.yunxin.kit.conversationkit.ui.common.ConversationConstant.LIB_TAG;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.netease.nimlib.sdk.friend.model.MuteListChangedNotify;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.team.model.Team;
import com.netease.yunxin.kit.alog.ALog;
import com.netease.yunxin.kit.chatkit.model.ConversationInfo;
import com.netease.yunxin.kit.common.ui.viewholder.ViewHolderClickListener;
import com.netease.yunxin.kit.conversationkit.ui.IConversationFactory;
import com.netease.yunxin.kit.conversationkit.ui.R;
import com.netease.yunxin.kit.conversationkit.ui.model.ConversationBean;
import com.netease.yunxin.kit.conversationkit.ui.page.interfaces.ILoadListener;
import com.netease.yunxin.kit.corekit.im.model.FriendInfo;
import com.netease.yunxin.kit.corekit.im.model.UserInfo;
import java.util.Comparator;
import java.util.List;
/**
* conversation list view
*/
public class ConversationView extends FrameLayout {
private final String TAG = "ConversationView";
private RecyclerView recyclerView;
private ConversationAdapter adapter;
private ILoadListener loadMoreListener;
private final int LOAD_MORE_DIFF = 5;
public ConversationView(Context context) {
super(context);
init(null);
}
public ConversationView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public ConversationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
recyclerView = new RecyclerView(getContext());
recyclerView.setId(R.id.conversation_rv);
this.addView(
recyclerView,
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
adapter = new ConversationAdapter(layoutManager);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int position = layoutManager.findLastVisibleItemPosition();
if (loadMoreListener != null
&& loadMoreListener.hasMore()
&& adapter.getItemCount() < position + LOAD_MORE_DIFF) {
ConversationBean last = adapter.getData(adapter.getItemCount() - 1);
loadMoreListener.loadMore(last);
}
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
}
public void setLoadMoreListener(ILoadListener listener) {
this.loadMoreListener = listener;
}
public void setItemClickListener(ViewHolderClickListener listener) {
adapter.setViewHolderClickListener(listener);
}
public void addItemDecoration(RecyclerView.ItemDecoration decoration) {
recyclerView.addItemDecoration(decoration);
}
public void setViewHolderFactory(IConversationFactory factory) {
adapter.setViewHolderFactory(factory);
}
public void setComparator(Comparator<ConversationInfo> comparator) {
this.adapter.setComparator(comparator);
}
public void setData(List<ConversationBean> data) {
if (adapter != null) {
adapter.setData(data);
}
}
public void addData(List<ConversationBean> data) {
if (adapter != null) {
adapter.appendData(data);
}
}
public void update(List<ConversationBean> data) {
if (adapter != null) {
ALog.d(LIB_TAG, TAG, "update ConversationBean list, start");
adapter.update(data);
ALog.d(LIB_TAG, TAG, "update ConversationBean list, end");
}
}
public void update(ConversationBean data) {
if (adapter != null) {
ALog.d(LIB_TAG, TAG, "update ConversationBean, start");
adapter.update(data);
ALog.d(LIB_TAG, TAG, "update ConversationBean, end");
}
}
public void updateSelector(ConversationBean data) {
if (adapter != null) {
adapter.updateSelector(data);
}
}
public void updateUserInfo(List<UserInfo> data) {
if (adapter != null) {
adapter.updateUserInfo(data);
}
}
public void updateFriendInfo(List<FriendInfo> data) {
if (adapter != null) {
adapter.updateFriendInfo(data);
}
}
public void updateTeamInfo(List<Team> data) {
if (adapter != null) {
adapter.updateTeamInfo(data);
}
}
public void updateTeamInfoType(List<Team> data,boolean isMyGroup) {
if (adapter != null) {
adapter.updateTeamInfoType(data,isMyGroup);
}
}
public void updateMuteInfo(MuteListChangedNotify changedNotify) {
if (adapter != null) {
adapter.updateMuteInfo(changedNotify);
}
}
public void remove(List<ConversationBean> data) {
if (adapter != null) {
adapter.removeData(data);
}
}
public void removeAll() {
if (adapter != null) {
adapter.removeAll();
}
}
public int getDataSize() {
if (adapter != null) {
return adapter.getItemCount();
}
return 0;
}
public void removeConversation(String id) {
if (adapter != null) {
adapter.removeData(id);
}
}
public void updateAit(List<String> idList) {
if (adapter != null) {
adapter.updateAit(idList);
}
}
public void addStickTop(String id) {
if (adapter != null) {
adapter.addStickTop(id);
}
}
public void removeStickTop(String id) {
if (adapter != null) {
adapter.removeStickTop(id);
}
}
public void setShowTag(boolean show) {
if (adapter != null) {
adapter.setShowTag(show);
}
}
public ConversationAdapter getAdatper() {
return adapter;
}
//设置全部已读回执
public void setMakerReadAll() {
if (adapter != null) {
List<ConversationBean> listAll = adapter.getConversationList();
if (listAll != null && listAll.size() > 0) {
for (ConversationBean bean : listAll) {
ConversationInfo info = bean.infoData;
if (info.getSessionType() == SessionTypeEnum.P2P) {
} else if (info.getSessionType() == SessionTypeEnum.Team
|| info.getSessionType() == SessionTypeEnum.SUPER_TEAM) {
}
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="@color/color_6877fe" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<stroke
android:width="@dimen/dimen_1_dp"
android:color="@color/color_6877fe" />
</shape>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<selector xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" tools:ignore="MissingDefaultResource">
<item android:drawable="@color/fun_conversation_item_bg_color" android:state_pressed="false" />
<item android:drawable="@color/fun_conversation_item_stick_bg_color" android:state_pressed="true" />
<item android:drawable="@color/fun_conversation_item_bg_color"/>
</selector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<selector xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" tools:ignore="MissingDefaultResource">
<item android:drawable="@color/fun_conversation_item_stick_bg_color" />
</selector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:gravity="top|end" android:top="2dp" android:right="17dp">
<rotate android:fromDegrees="45">
<shape android:shape="rectangle">
<solid android:color="@color/fun_conversation_add_pop_bg_color"/>
<size android:height="10.45dp" android:width="10.45dp"/>
</shape>
</rotate>
</item>
<item android:top="5dp">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="@color/fun_conversation_add_pop_bg_color" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,15 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M6.9706,2.7743C9.12,2.7777 10.9176,4.4086 11.1293,6.5476C11.3413,8.6866 9.8984,10.6385 7.7916,11.0634C5.6845,11.4884 3.598,10.2484 2.9643,8.1945C2.3305,6.1405 3.3552,3.9405 5.3352,3.1042C5.8526,2.8853 6.4088,2.7731 6.9706,2.7743ZM6.9706,1.8046C4.1144,1.8046 1.7988,4.1201 1.7988,6.9763C1.7988,9.8324 4.1144,12.148 6.9706,12.148C9.8267,12.148 12.1423,9.8324 12.1423,6.9763C12.1423,4.1201 9.8267,1.8046 6.9706,1.8046ZM13.7099,14.2005C13.6462,14.2007 13.5831,14.1881 13.5243,14.1637C13.4654,14.1393 13.412,14.1035 13.3671,14.0583L10.9105,11.6017C10.7269,11.4114 10.7295,11.109 10.9164,10.9219C11.1035,10.7349 11.4059,10.7322 11.596,10.9161L14.0526,13.3726C14.1203,13.4405 14.1665,13.5268 14.1852,13.6209C14.2039,13.7149 14.1943,13.8123 14.1576,13.9009C14.121,13.9896 14.059,14.0654 13.9793,14.1187C13.8996,14.172 13.8058,14.2005 13.7099,14.2005Z"
android:fillColor="#A6ADB6"/>
</vector>

View File

@@ -0,0 +1,4 @@
<vector android:height="20dp" android:viewportHeight="21"
android:viewportWidth="21" android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FC596A" android:pathData="M10.5,0C4.706,0 0,4.706 0,10.5C0,16.294 4.706,21 10.5,21C16.294,21 21,16.294 21,10.5C21,4.706 16.294,0 10.5,0ZM11.25,16.5H9.75V15H11.25V16.5ZM9.75,13.5V4.5H11.25V13.5H9.75Z"/>
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:ignore="MissingDefaultResource">
<FrameLayout
android:id="@+id/conversation_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<LinearLayout
android:id="@+id/topLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="MissingConstraints">
<com.netease.yunxin.kit.common.ui.widgets.TitleBarView
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_52_dp"
android:visibility="gone"
app:head_img_visible="visible"
app:head_title="@string/conversation_title"
app:head_title_color="@color/color_black"
tools:ignore="MissingConstraints" />
<com.netease.yunxin.kit.common.ui.widgets.RoundFrameLayout
android:id="@+id/searchLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_36_dp"
android:layout_marginStart="@dimen/dimen_8_dp"
android:visibility="gone"
android:layout_marginEnd="@dimen/dimen_8_dp"
android:background="@color/color_white"
app:corner_radius="@dimen/dimen_4_dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/dimen_20_dp"
android:layout_height="@dimen/dimen_20_dp"
android:layout_gravity="center_vertical"
android:src="@drawable/fun_ic_conversation_search" />
<TextView
android:layout_width="wrap_content"
android:layout_height="@dimen/dimen_36_dp"
android:layout_marginStart="@dimen/dimen_5_dp"
android:gravity="center_vertical"
android:text="@string/fun_conversation_search_text"
android:textColor="@color/fun_conversation_search_text_color"
android:textSize="@dimen/text_size_16" />
</LinearLayout>
</com.netease.yunxin.kit.common.ui.widgets.RoundFrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/bodyTopLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
<LinearLayout
android:id="@+id/bodyLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/errorTv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_fceeee"
android:gravity="center"
android:paddingStart="@dimen/dimen_35_dp"
android:paddingTop="@dimen/dimen_10_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:paddingBottom="@dimen/dimen_10_dp"
android:text="@string/conversation_network_error_tip"
android:textColor="@color/color_50_000000"
android:textSize="14dp"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_error" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.netease.yunxin.kit.conversationkit.ui.view.ConversationView
android:id="@+id/conversationView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/emptyLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_188_dp"
android:layout_marginTop="@dimen/dimen_150_dp"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:src="@drawable/fun_ic_conversation_empty" />
<TextView
android:id="@+id/empty_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/conversation_empty_tip" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/bottomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
</LinearLayout>

View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="76dp"
android:background="@drawable/fun_conversation_view_holder_selector"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_selector"
android:layout_width="@dimen/dimen_20_dp"
android:layout_height="@dimen/dimen_20_dp"
android:layout_marginStart="@dimen/fun_conversation_margin_15dp"
android:background="@drawable/fun_ic_unselector_conversation"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/avatarView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="@+id/avatarView" />
<View
android:id="@+id/view_selectorclick"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/avatarView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/avatarView"
app:layout_constraintTop_toTopOf="@+id/avatarView" />
<com.netease.yunxin.kit.common.ui.widgets.ContactAvatarView
android:id="@+id/avatarView"
android:layout_width="@dimen/dimen_45_dp"
android:layout_height="@dimen/dimen_45_dp"
android:layout_marginStart="@dimen/fun_conversation_margin_15dp"
android:layout_marginTop="15dp"
app:avatarCorner="@dimen/dimen_45_dp"
app:layout_constraintLeft_toRightOf="@+id/iv_selector"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/contentLayout"
android:layout_width="0dp"
android:layout_height="@dimen/dimen_45_dp"
android:layout_marginVertical="15dp"
android:layout_marginStart="@dimen/fun_conversation_margin_15dp"
android:layout_marginEnd="@dimen/fun_conversation_margin_15dp"
app:layout_constraintLeft_toRightOf="@+id/avatarView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/ll_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/nameTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:maxWidth="@dimen/dimen_120_dp"
android:singleLine="true"
android:textColor="@color/fun_conversation_item_title_text_color"
android:textSize="@dimen/text_size_16" />
<TextView
android:id="@+id/aliasTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/fun_conversation_item_title_text_color"
android:textSize="@dimen/text_size_16"
android:visibility="gone"
/>
</LinearLayout>
<!-- <TextView-->
<!-- android:id="@+id/groupNumber"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="@dimen/dimen_5_dp"-->
<!-- android:layout_marginEnd="@dimen/dimen_16_dp"-->
<!-- android:textColor="@color/fun_conversation_item_title_text_color"-->
<!-- android:textSize="@dimen/text_size_16"-->
<!-- android:textStyle="bold"-->
<!-- android:visibility="visible"-->
<!-- app:layout_constraintLeft_toRightOf="@+id/muteIv"-->
<!-- app:layout_constraintRight_toLeftOf="@+id/timeTv"-->
<!-- app:layout_constraintTop_toTopOf="parent" />-->
<LinearLayout
android:id="@+id/messageLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/dimen_7_dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/unreadTv">
<TextView
android:id="@+id/aitTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dimen_4_dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/conversation_ait_tip"
android:textColor="@color/color_ff4e54"
android:textSize="@dimen/text_size_14"
android:visibility="gone" />
<TextView
android:id="@+id/draftTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/dimen_4_dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/conversation_draft_tip"
android:textColor="@color/color_ff4e54"
android:textSize="@dimen/text_size_14"
android:visibility="gone" />
<TextView
android:id="@+id/draftTvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/text_size_14"
android:visibility="gone" />
<TextView
android:id="@+id/messageTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/text_size_14" />
</LinearLayout>
<TextView
android:id="@+id/timeTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:ellipsize="end"
android:maxWidth="@dimen/dimen_100_dp"
android:maxLines="1"
android:textColor="@color/fun_conversation_item_time_text_color"
android:textSize="@dimen/text_size_12"
app:layout_constraintBottom_toBottomOf="@+id/ll_name"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/ll_name" />
<ImageView
android:id="@+id/muteIv"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginLeft="@dimen/dimen_5_dp"
android:src="@drawable/fun_ic_conversation_mute"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/ll_name"
app:layout_constraintLeft_toRightOf="@+id/ll_name"
app:layout_constraintTop_toTopOf="@+id/ll_name" />
<TextView
android:id="@+id/tv_myteamtype"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dimen_5_dp"
android:layout_marginEnd="@dimen/dimen_10_dp"
android:ellipsize="middle"
android:maxWidth="@dimen/dimen_60_dp"
android:paddingStart="@dimen/dimen_5_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:singleLine="true"
android:textSize="@dimen/text_size_12"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/ll_name"
app:layout_constraintLeft_toRightOf="@+id/muteIv"
app:layout_constraintTop_toTopOf="@+id/ll_name" />
<TextView
android:id="@+id/unreadTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dimen_2_dp"
android:background="@drawable/bg_conversation_red_dot"
android:gravity="center"
android:paddingStart="@dimen/dimen_5_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:textColor="@color/color_white"
android:textSize="@dimen/text_size_12"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/messageLayout"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#EDE3E3"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MissingDefaultResource">
<LinearLayout
android:id="@+id/topLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.netease.yunxin.kit.common.ui.widgets.TitleBarView
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_42_dp"
app:head_img_visible="visible"
app:head_title="@string/conversation_title"
app:head_title_color="@color/color_black"
tools:ignore="MissingConstraints" />
<com.netease.yunxin.kit.common.ui.widgets.RoundFrameLayout
android:id="@+id/searchLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_white"
app:corner_radius="@dimen/dimen_4_dp"
android:layout_marginStart="@dimen/dimen_16_dp"
android:layout_marginEnd="@dimen/dimen_16_dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="@dimen/dimen_36_dp"
android:text="@string/fun_conversation_search_text"
android:layout_gravity="center"
android:gravity="center"
android:drawablePadding="@dimen/dimen_8_dp"
android:drawableStart="@drawable/fun_ic_conversation_search" />
</com.netease.yunxin.kit.common.ui.widgets.RoundFrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/bodyTopLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/bodyLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/errorTv"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/color_fee3e6"
android:gravity="center"
android:text="@string/conversation_network_error_tip"
android:textColor="@color/color_fc596a"
android:textSize="14dp"
android:visibility="gone" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.netease.yunxin.kit.conversationkit.ui.view.ConversationView
android:id="@+id/conversationView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout android:id="@+id/emptyLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_188_dp"
android:layout_marginTop="@dimen/dimen_150_dp"
android:visibility="gone"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dimen_118_dp"
android:layout_height="@dimen/dimen_96_dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_conversation_empty"/>
<TextView android:id="@+id/empty_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/conversation_empty_tip"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/bottomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="fun_conversation_page_bg_color">#ededed</color>
<color name="fun_conversation_secondary_page_bg_color">#ededed</color>
<color name="fun_conversation_search_text_color">#4d000000</color>
<color name="fun_conversation_item_bg_color">#FFFBFB</color>
<color name="fun_conversation_item_title_text_color">#333333</color>
<color name="fun_conversation_item_sub_title_text_color">#7F7B81</color>
<color name="fun_conversation_item_time_text_color">#7f7b81</color>
<color name="fun_conversation_item_divide_line_color">#d8d8d8</color>
<!-- 置顶会话背景色 -->
<color name="fun_conversation_item_stick_bg_color">#ededef</color>
<!-- 创建群聊和添加好友PopupWindow 字体颜色 -->
<color name="fun_conversation_add_pop_text_color">#ffffff</color>
<color name="fun_conversation_add_pop_bg_color">#4c4c4c</color>
<color name="color_6877fe">#6877fe</color>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fun_add_pop_item_height">50dp</dimen>
<dimen name="fun_add_pop_item_width">126dp</dimen>
<dimen name="fun_add_pop_item_margin_right_top">16dp</dimen>
<dimen name="fun_add_pop_item_padding">8dp</dimen>
<dimen name="fun_conversation_margin_15dp">15dp</dimen>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<selector xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" tools:ignore="MissingDefaultResource">
<item android:drawable="@color/color_white" android:state_pressed="false" />
<item android:drawable="@color/color_ededef" android:state_pressed="true" />
</selector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<selector xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" tools:ignore="MissingDefaultResource">
<item android:drawable="@color/color_ededef" />
</selector>

View File

@@ -0,0 +1,36 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M9.0805,10.5H13"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M10.9999,8.5808L10.9999,12.5003"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M6.4598,3.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M8.5324,8.0002H4.7499C2.6789,8.0002 0.9999,9.6792 0.9999,11.7502V11.7502C0.9999,12.4406 1.5596,13.0002 2.2499,13.0002H8.5324"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,36 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M11.8972,3.9999C11.8972,5.2141 10.9129,6.1985 9.6986,6.1985C8.4844,6.1985 7.5,5.2141 7.5,3.9999C7.5,2.7856 8.4844,1.8013 9.6986,1.8013C10.9129,1.8013 11.8972,2.7856 11.8972,3.9999Z"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M4.8888,5.3892C4.8888,6.1562 4.267,6.778 3.4999,6.778C2.7329,6.778 2.111,6.1562 2.111,5.3892C2.111,4.6221 2.7329,4.0002 3.4999,4.0002C4.267,4.0002 4.8888,4.6221 4.8888,5.3892Z"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M3.0181,8.7217H2.5833C1.4327,8.7217 0.4999,9.6544 0.4999,10.805V10.805C0.4999,11.1886 0.8109,11.4995 1.1944,11.4995H3.0181"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M5.5,11.5C5.5,9.567 7.067,8 9,8H10C11.933,8 13.5,9.567 13.5,11.5C13.5,12.0523 13.0523,12.5 12.5,12.5H6.5C5.9477,12.5 5.5,12.0523 5.5,11.5Z"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,48 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M10,10H13"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M11.5,8.5L11.5,11.5"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M9.8014,3.922m-2.1986,0a2.1986,2.1986 0,1 1,4.3972 0a2.1986,2.1986 0,1 1,-4.3972 0"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M3.8888,5.8889m-1.3889,0a1.3889,1.3889 0,1 1,2.7778 0a1.3889,1.3889 0,1 1,-2.7778 0"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M8.8653,7.8794H8.2977C6.4763,7.8794 4.9998,9.3559 4.9998,11.1773V11.1773C4.9998,11.7844 5.4919,12.2766 6.0991,12.2766H8.9858"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M3.5182,9.2227H3.0834C1.9327,9.2227 1,10.1554 1,11.306V11.306C1,11.6895 1.3109,12.0005 1.6945,12.0005H3.5182"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,39 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:strokeWidth="1"
android:pathData="M0.5,2.5L12,13"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M11.4737,10.5005V6.5C11.4737,3.8839 9.3529,1.7632 6.7368,1.7632C5.5701,1.7632 4.5019,2.185 3.6764,2.8845M2,6.5V10.5005H6.7368"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M0.8125,10.5C3.431,10.5 4.9561,10.5 7,10.5M13,10.5C12.2123,10.5 12.197,10.5 11.5,10.5"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M5.5,13H8.5"
android:fillColor="#00000000"
android:strokeColor="#656A72"
android:strokeLineCap="round"/>
<path
android:pathData="M6,1C6,0.4477 6.4477,0 7,0C7.5523,0 8,0.4477 8,1V1.5H6V1Z"
android:fillColor="#656A72"/>
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:ignore="MissingDefaultResource">
<FrameLayout
android:id="@+id/conversation_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/topLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="MissingConstraints">
<com.netease.yunxin.kit.common.ui.widgets.TitleBarView
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_52_dp"
app:head_img_visible="visible"
app:head_title="@string/conversation_title"
app:head_title_color="@color/color_black"
tools:ignore="MissingConstraints" />
<View
android:id="@+id/conversationLine"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_1_dp"
android:alpha="0.6"
android:background="@color/color_e9eff5"
tools:ignore="MissingConstraints" />
</LinearLayout>
<FrameLayout
android:id="@+id/bodyTopLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
<LinearLayout
android:id="@+id/bodyLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
tools:ignore="MissingConstraints">
<TextView
android:id="@+id/errorTv"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/color_fee3e6"
android:gravity="center"
android:text="@string/conversation_network_error_tip"
android:textColor="@color/color_fc596a"
android:textSize="14dp"
android:visibility="gone" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.netease.yunxin.kit.conversationkit.ui.view.ConversationView
android:id="@+id/conversationView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout android:id="@+id/emptyLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_188_dp"
android:layout_marginTop="@dimen/dimen_150_dp"
android:visibility="gone"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dimen_118_dp"
android:layout_height="@dimen/dimen_96_dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_conversation_empty"/>
<TextView android:id="@+id/empty_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/conversation_empty_tip"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/bottomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
</LinearLayout>

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rootView"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_62_dp">
<FrameLayout
android:id="@+id/avatarLayout"
android:layout_width="@dimen/dimen_52_dp"
android:layout_height="@dimen/dimen_42_dp"
android:layout_marginVertical="@dimen/dimen_10_dp"
android:layout_marginStart="@dimen/dimen_20_dp">
<com.netease.yunxin.kit.common.ui.widgets.ContactAvatarView
android:id="@+id/avatarView"
android:layout_width="@dimen/dimen_42_dp"
android:layout_height="@dimen/dimen_42_dp" />
<TextView android:id="@+id/unreadTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="@dimen/dimen_5_dp"
android:background="@drawable/bg_conversation_red_dot"
android:gravity="center"
android:paddingStart="@dimen/dimen_5_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:textColor="@color/color_white"
android:textSize="@dimen/text_size_12"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dimen_74_dp"
android:layout_marginEnd="@dimen/dimen_20_dp">
<TextView
android:id="@+id/nameTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_10_dp"
android:layout_marginEnd="@dimen/dimen_100_dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/color_333333"
android:textSize="@dimen/text_size_16" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_35_dp"
android:layout_marginEnd="@dimen/dimen_18_dp"
android:orientation="horizontal">
<TextView
android:id="@+id/aitTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginEnd="@dimen/dimen_4_dp"
android:textColor="@color/color_ff4e54"
android:visibility="gone"
android:text="@string/conversation_ait_tip"/>
<TextView
android:id="@+id/messageTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/color_999999"/>
</LinearLayout>
<TextView
android:id="@+id/timeTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen_12_dp"
android:ellipsize="end"
android:maxWidth="@dimen/dimen_100_dp"
android:maxLines="1"
android:textColor="@color/color_cccccc"
android:textSize="@dimen/text_size_12" />
<ImageView android:id="@+id/muteIv"
android:layout_width="@dimen/dimen_12_dp"
android:layout_height="@dimen/dimen_12_dp"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen_38_dp"
android:src="@drawable/ic_conversation_mute"
android:visibility="gone"
/>
</FrameLayout>
</FrameLayout>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_62_dp">
<FrameLayout
android:id="@+id/conversation_avatar_fl"
android:layout_width="@dimen/dimen_52_dp"
android:layout_height="@dimen/dimen_42_dp"
android:layout_marginVertical="@dimen/dimen_10_dp"
android:layout_marginStart="@dimen/dimen_20_dp">
<com.netease.yunxin.kit.common.ui.widgets.ContactAvatarView
android:id="@+id/avatar_view"
android:layout_width="@dimen/dimen_42_dp"
android:layout_height="@dimen/dimen_42_dp" />
<TextView android:id="@+id/conversation_unread_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="@dimen/dimen_5_dp"
android:background="@drawable/bg_conversation_red_dot"
android:gravity="center"
android:paddingStart="@dimen/dimen_5_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:textColor="@color/color_white"
android:textSize="@dimen/text_size_12"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/conversation_time_fl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dimen_74_dp"
android:layout_marginEnd="@dimen/dimen_20_dp">
<TextView
android:id="@+id/conversation_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_10_dp"
android:layout_marginEnd="@dimen/dimen_100_dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/color_333333"
android:textSize="@dimen/text_size_16" />
<TextView
android:id="@+id/conversation_message_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginTop="@dimen/dimen_35_dp"
android:layout_marginEnd="@dimen/dimen_18_dp"
android:textColor="@color/color_999999" />
<TextView
android:id="@+id/conversation_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen_12_dp"
android:ellipsize="end"
android:maxWidth="@dimen/dimen_100_dp"
android:maxLines="1"
android:textColor="@color/color_cccccc"
android:textSize="@dimen/text_size_12" />
<ImageView android:id="@+id/conversation_mute_iv"
android:layout_width="@dimen/dimen_12_dp"
android:layout_height="@dimen/dimen_12_dp"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen_38_dp"
android:src="@drawable/ic_conversation_mute"
android:visibility="gone"
/>
</FrameLayout>
</FrameLayout>

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_62_dp">
<FrameLayout
android:id="@+id/conversation_avatar_fl"
android:layout_width="@dimen/dimen_52_dp"
android:layout_height="@dimen/dimen_42_dp"
android:layout_marginVertical="@dimen/dimen_10_dp"
android:layout_marginStart="@dimen/dimen_20_dp">
<com.netease.yunxin.kit.common.ui.widgets.ContactAvatarView
android:id="@+id/avatar_view"
android:layout_width="@dimen/dimen_42_dp"
android:layout_height="@dimen/dimen_42_dp" />
<TextView android:id="@+id/conversation_unread_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="@dimen/dimen_5_dp"
android:background="@drawable/bg_conversation_red_dot"
android:gravity="center"
android:paddingStart="@dimen/dimen_5_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:textColor="@color/color_white"
android:textSize="@dimen/text_size_12"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/conversation_time_fl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dimen_74_dp"
android:layout_marginEnd="@dimen/dimen_20_dp">
<TextView
android:id="@+id/conversation_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_10_dp"
android:layout_marginEnd="@dimen/dimen_100_dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/color_333333"
android:textSize="@dimen/text_size_16" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_35_dp"
android:layout_marginEnd="@dimen/dimen_18_dp"
android:orientation="horizontal">
<TextView
android:id="@+id/conversation_ait_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_marginEnd="@dimen/dimen_4_dp"
android:textColor="@color/color_f24957"
android:visibility="gone"
android:text="@string/conversation_ait_tip"/>
<TextView
android:id="@+id/conversation_message_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/color_999999"/>
</LinearLayout>
<TextView
android:id="@+id/conversation_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen_12_dp"
android:ellipsize="end"
android:maxWidth="@dimen/dimen_100_dp"
android:maxLines="1"
android:textColor="@color/color_cccccc"
android:textSize="@dimen/text_size_12" />
<ImageView android:id="@+id/conversation_mute_iv"
android:layout_width="@dimen/dimen_12_dp"
android:layout_height="@dimen/dimen_12_dp"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen_38_dp"
android:src="@drawable/ic_conversation_mute"
android:visibility="gone"
/>
</FrameLayout>
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/dimen_10_dp" />
<solid android:color="@color/color_cccccc" />
</shape>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/dimen_10_dp" />
<solid android:color="@color/color_ff4e54" />
</shape>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<selector xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" tools:ignore="MissingDefaultResource">
<item android:drawable="@color/color_white" android:state_pressed="false" />
<item android:drawable="@color/color_ededef" android:state_pressed="true" />
</selector>

View File

@@ -0,0 +1,15 @@
<!-- Copyright (c) 2022 NetEase, Inc. All rights reserved. -->
<!-- Use of this source code is governed by a MIT license that can be -->
<!-- found in the LICENSE file. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:strokeWidth="1"
android:pathData="M9,9m-8.5,0a8.5,8.5 0,1 1,17 0a8.5,8.5 0,1 1,-17 0"
android:fillColor="#00000000"
android:strokeColor="#969AA0" />
</vector>

View File

@@ -0,0 +1,16 @@
<!-- Copyright (c) 2022 NetEase, Inc. All rights reserved. -->
<!-- Use of this source code is governed by a MIT license that can be -->
<!-- found in the LICENSE file. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,9m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
android:fillColor="#337EFF" />
<path
android:pathData="M5.1508,8.7407C4.7315,8.3813 4.1002,8.4299 3.7407,8.8492C3.3813,9.2685 3.4299,9.8998 3.8492,10.2593L5.1508,8.7407ZM8,12.5L7.3492,13.2593C7.7587,13.6102 8.3727,13.5733 8.7372,13.1757L8,12.5ZM14.2372,7.1757C14.6103,6.7686 14.5828,6.136 14.1757,5.7628C13.7686,5.3896 13.136,5.4172 12.7628,5.8243L14.2372,7.1757ZM3.8492,10.2593L7.3492,13.2593L8.6508,11.7407L5.1508,8.7407L3.8492,10.2593ZM8.7372,13.1757L14.2372,7.1757L12.7628,5.8243L7.2628,11.8243L8.7372,13.1757Z"
android:fillColor="#ffffff" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (c) 2022 NetEase, Inc. All rights reserved. -->
<!-- Use of this source code is governed by a MIT license that can be -->
<!-- found in the LICENSE file. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/conversation_radio_button_selected" android:state_checked="true" />
<item android:drawable="@drawable/conversation_radio_button_not_selected" android:state_checked="false" />
</selector>

View File

@@ -0,0 +1,17 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:strokeWidth="1"
android:pathData="M9,9m-8.5,0a8.5,8.5 0,1 1,17 0a8.5,8.5 0,1 1,-17 0"
android:fillColor="#00000000"
android:strokeColor="#969AA0" />
</vector>

View File

@@ -0,0 +1,18 @@
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,9m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
android:fillColor="#337EFF" />
<path
android:pathData="M5.1508,8.7407C4.7315,8.3813 4.1002,8.4299 3.7407,8.8492C3.3813,9.2685 3.4299,9.8998 3.8492,10.2593L5.1508,8.7407ZM8,12.5L7.3492,13.2593C7.7587,13.6102 8.3727,13.5733 8.7372,13.1757L8,12.5ZM14.2372,7.1757C14.6103,6.7686 14.5828,6.136 14.1757,5.7628C13.7686,5.3896 13.136,5.4172 12.7628,5.8243L14.2372,7.1757ZM3.8492,10.2593L7.3492,13.2593L8.6508,11.7407L5.1508,8.7407L3.8492,10.2593ZM8.7372,13.1757L14.2372,7.1757L12.7628,5.8243L7.2628,11.8243L8.7372,13.1757Z"
android:fillColor="#ffffff" />
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_conversation_radio_button_selected" android:state_checked="true" />
<item android:drawable="@drawable/ic_conversation_radio_button_not_selected" android:state_checked="false" />
</selector>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.netease.yunxin.kit.common.ui.widgets.BackTitleBar
android:id="@+id/conversation_selector_title_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_60_dp"
app:leftTitleText="@string/cancel_title"
app:rightTitleText="@string/sure_title"
tools:ignore="MissingConstraints" />
<TextView android:id="@+id/conversation_subtitle_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_12"
android:textColor="@color/color_999999"
android:text="@string/recent_title"
android:layout_marginLeft="@dimen/dimen_20_dp"
/>
<com.netease.yunxin.kit.conversationkit.ui.view.ConversationView
android:id="@+id/conversation_selector_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/dimen_12_dp"/>
</LinearLayout>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MissingDefaultResource">
<LinearLayout
android:id="@+id/conversation_top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.netease.yunxin.kit.common.ui.widgets.TitleBarView
android:id="@+id/conversation_title_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_52_dp"
app:head_img_visible="visible"
app:head_title="@string/conversation_title"
app:head_title_color="@color/color_black"
tools:ignore="MissingConstraints" />
<View
android:id="@+id/conversation_line"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_1_dp"
android:alpha="0.6"
android:background="@color/color_e9eff5"
tools:ignore="MissingConstraints" />
</LinearLayout>
<FrameLayout
android:id="@+id/conversation_body_top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/conversation_body_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/conversation_network_error_tv"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/color_fee3e6"
android:gravity="center"
android:text="@string/conversation_network_error_tip"
android:textColor="@color/color_fc596a"
android:textSize="14dp"
android:visibility="gone" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.netease.yunxin.kit.conversationkit.ui.view.ConversationView
android:id="@+id/conversation_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout android:id="@+id/conversation_empty_view"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_188_dp"
android:layout_marginTop="@dimen/dimen_150_dp"
android:visibility="gone"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dimen_118_dp"
android:layout_height="@dimen/dimen_96_dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_conversation_empty"/>
<TextView android:id="@+id/empty_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/conversation_empty_tip"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/conversation_bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:background="@drawable/conversation_common_view_holder_selector"
android:orientation="horizontal"
android:layout_height="@dimen/dimen_62_dp">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/conversation_selector_cb"
android:layout_width="@dimen/dimen_18_dp"
android:layout_height="@dimen/dimen_18_dp"
android:layout_marginStart="@dimen/dimen_20_dp"
android:layout_gravity="center_vertical"
style="@style/ConversationSelectorRadioBtn"
android:clickable="false"/>
<FrameLayout
android:id="@+id/conversation_avatar_fl"
android:layout_width="@dimen/dimen_52_dp"
android:layout_height="@dimen/dimen_42_dp"
android:layout_marginVertical="@dimen/dimen_10_dp"
android:layout_marginStart="@dimen/dimen_20_dp">
<com.netease.yunxin.kit.common.ui.widgets.ContactAvatarView
android:id="@+id/avatar_view"
android:layout_width="@dimen/dimen_42_dp"
android:layout_height="@dimen/dimen_42_dp" />
<TextView android:id="@+id/conversation_unread_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="@dimen/dimen_5_dp"
android:background="@drawable/bg_conversation_red_dot"
android:gravity="center"
android:paddingStart="@dimen/dimen_5_dp"
android:paddingEnd="@dimen/dimen_5_dp"
android:textColor="@color/color_white"
android:textSize="@dimen/text_size_12"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/conversation_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen_10_dp"
android:layout_marginEnd="@dimen/dimen_100_dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/color_333333"
android:textSize="@dimen/text_size_16" />
</LinearLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be found in the LICENSE file.
-->
<resources>
<string name="conversation_title">CommsEase IM</string>
<string name="add_friend">Add Contacts</string>
<string name="create_group_team">Create Temp Group</string>
<string name="create_advanced_team"> Create Group</string>
<string name="create_advanced_team_success">Create group successfully</string>
<string name="stick_title">Sticky on Top</string>
<string name="cancel_stick_title">Unpin</string>
<string name="delete_title">Delete this chat</string>
<string name="hide_title">Hide this chat</string>
<string name="recent_title">Last chat</string>
<string name="cancel_title">Cancel </string>
<string name="sure_title">Yes</string>
<string name="sure_count_title">Yes(%d)</string>
<string name="conversation_network_error_tip">No Internet</string>
<string name="conversation_empty_tip">No Chat</string>
<string name="conversation_service_empty_tip">No customer service session record yet</string>
<string name="conversation_ait_tip">[You were mentioned]</string>
<string name="conversation_draft_tip">[Draft]</string>
<!-- message type-->
<string name="msg_type_location">[Location Message]</string>
<string name="msg_type_rtc_video">[Video Call]</string>
<string name="msg_type_rtc_audio">[Audio Call]</string>
<string name="msg_type_image">[Photo Message]</string>
<string name="msg_type_audio">[Voice Message]</string>
<string name="msg_type_video">[Video Message]</string>
<string name="msg_type_file">[File Message]</string>
<string name="msg_type_notification">[Notification Message]</string>
<string name="msg_type_notification_announcement">[Group Announcement] %s</string>
<string name="msg_type_tip">[Tip Message]</string>
<string name="msg_type_custom">[Custom Message]</string>
<string name="msg_type_no_tips">[Unknown Message]</string>
<string name="fun_conversation_search_text">Search</string>
<string name="time_zuotian_txt">Yesterday</string>
<string name="mine_text">Mine</string>
<string name="chat_message_revoke_content">recalled the message</string>
<string name="services_text">Service</string>
</resources>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<resources>
<string name="conversation_title">クラウド IM</string>
<string name="add_friend">友達を追加</string>
<string name="create_group_team">ディスカッション グループを作成する</string>
<string name="create_advanced_team">高度なグループを作成する</string>
<string name="create_advanced_team_success">グループ チャットが正常に作成されました</string>
<string name="stick_title">このチャットを選択してください</string>
<string name="cancel_stick_title">スティックをキャンセル</string>
<string name="delete_title">このチャットを削除</string>
<string name="hide_title">このチャットを非表示にする</string>
<string name="recent_title">最近のチャット</string>
<string name="cancel_title">キャンセル</string>
<string name="sure_title">OK</string>
<string name="sure_count_title">確認(%d)</string>
<string name="conversation_network_error_tip">ネットワークは現在利用できません。ネットワーク設定を確認してください。 </string>
<string name="conversation_empty_tip">まだ会話がありません</string>
<string name="conversation_service_empty_tip">カスタマーサービスセッションの記録はまだありません</string>
<string name="conversation_ait_tip">[Someone@me]</string>
<string name="conversation_draft_tip">[下書き]</string>
<!-- メッセージ タイプ -->
<string name="msg_type_location">[地理的位置]</string>
<string name="msg_type_rtc_video">[ビデオ通話]</string>
<string name="msg_type_rtc_audio">[音声通話]</string>
<string name="msg_type_image">[画像メッセージ]</string>
<string name="msg_type_audio">[音声メッセージ]</string>
<string name="msg_type_video">[ビデオメッセージ]</string>
<string name="msg_type_file">[ファイル メッセージ]</string>
<string name="msg_type_notification">[通知メッセージ]</string>
<string name="msg_type_notification_announcement">[グループのお知らせ] %s</string>
<string name="msg_type_tip">[リマインダーメッセージ]</string>
<string name="msg_type_custom">[カスタム メッセージ]</string>
<string name="msg_type_no_tips">[不明なメッセージ本文]</string>
<string name="fun_conversation_search_text">検索</string>
<string name="time_zuotian_txt">昨日</string>
<string name="mine_text">私の</string>
<string name="chat_message_revoke_content">がメッセージを撤回しました</string>
<string name="services_text">顧客サービス</string>
</resources>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<resources>
<string name="conversation_title">云信IM</string>
<string name="add_friend">添加好友</string>
<string name="create_group_team">创建讨论组</string>
<string name="create_advanced_team">创建高级群</string>
<string name="create_advanced_team_success">成功创建了群聊</string>
<string name="stick_title">置顶该聊天</string>
<string name="cancel_stick_title">取消置顶</string>
<string name="delete_title">删除该聊天</string>
<string name="hide_title">隐藏该聊天</string>
<string name="recent_title">最近聊天</string>
<string name="cancel_title">取消</string>
<string name="sure_title">确定</string>
<string name="sure_count_title">确定(%d)</string>
<string name="conversation_network_error_tip">当前网络不可用,请检查你当网络设置。</string>
<string name="conversation_empty_tip">暂无会话</string>
<string name="conversation_service_empty_tip">暂无客服会话记录</string>
<string name="conversation_ait_tip">[有人@我]</string>
<string name="conversation_draft_tip">[草稿]</string>
<!-- message type-->
<string name="msg_type_location">[地理位置]</string>
<string name="msg_type_rtc_video">[视频通话]</string>
<string name="msg_type_rtc_audio">[音频通话]</string>
<string name="msg_type_image">[图片消息]</string>
<string name="msg_type_audio">[语音消息]</string>
<string name="msg_type_video">[视频消息]</string>
<string name="msg_type_file">[文件消息]</string>
<string name="msg_type_notification">[通知消息]</string>
<string name="msg_type_notification_announcement">[群公告] %s</string>
<string name="msg_type_tip">[提醒消息]</string>
<string name="msg_type_custom">[自定义消息]</string>
<string name="msg_type_no_tips">[未知消息体]</string>
<string name="fun_conversation_search_text">搜索</string>
<string name="time_zuotian_txt">昨天</string>
<string name="mine_text">我的</string>
<string name="chat_message_revoke_content">撤回了一条消息</string>
<string name="services_text">客服</string>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="color_white">#FFFFFF</color>
<color name="color_333333">#333333</color>
<color name="color_337eff">#337eff</color>
<color name="color_ededef">#ededef</color>
<color name="color_666666">#666666</color>
<color name="color_ff4e54">#ff4e54</color>
<color name="color_999999">#999999</color>
<color name="color_cccccc">#cccccc</color>
<color name="color_9e9e9e">#9e9e9e</color>
<color name="color_e9eff5">#e9eff5</color>
<color name="color_fee3e6">#fee3e6</color>
<color name="color_fc596a">#fc596a</color>
<color name="color_50_000000">#7f000000</color>
<color name="color_fceeee">#fceeee</color>
<color name="color_5a5a5a">#5a5a5a</color>
<color name="color_7e7b84">#7E7B84</color>
<color name="color_f4f4f4">#f4f4f4</color>
</resources>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="pop_margin_right">-105dp</dimen>
<dimen name="pop_item_height">32dp</dimen>
<dimen name="pop_text_margin_left">15dp</dimen>
<dimen name="pop_text_margin_right_top">8dp</dimen>
<dimen name="alert_dialog_width">188dp</dimen>
<dimen name="dimen_62_dp">62dp</dimen>
<dimen name="dimen_60_dp">60dp</dimen>
<dimen name="dimen_38_dp">38dp</dimen>
<dimen name="dimen_52_dp">52dp</dimen>
<dimen name="dimen_42_dp">42dp</dimen>
<dimen name="dimen_5_dp">5dp</dimen>
<dimen name="dimen_74_dp">74dp</dimen>
<dimen name="dimen_100_dp">100dp</dimen>
<dimen name="dimen_18_dp">18dp</dimen>
<dimen name="dimen_8_dp">8dp</dimen>
<dimen name="dimen_14_dp">14dp</dimen>
<dimen name="text_size_12">12sp</dimen>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="conversation_rv" type="id"/>
</resources>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<resources>
<string name="conversation_title">雲端信IM</string>
<string name="add_friend">新增好友</string>
<string name="create_group_team">建立討論群組</string>
<string name="create_advanced_team">建立高級群</string>
<string name="create_advanced_team_success">成功建立了群組聊天</string>
<string name="stick_title">置頂該聊天</string>
<string name="cancel_stick_title">取消置頂</string>
<string name="delete_title">刪除該聊天</string>
<string name="hide_title">隱藏該聊天</string>
<string name="recent_title">最近聊天</string>
<string name="cancel_title">取消</string>
<string name="sure_title">確定</string>
<string name="sure_count_title">確定(%d)</string>
<string name="conversation_network_error_tip">目前網路不可用,請檢查你當網路設定。 </string>
<string name="conversation_empty_tip">暫無會話</string>
<string name="conversation_service_empty_tip">暫無客服會話記錄</string>
<string name="conversation_ait_tip">[有人@我]</string>
<string name="conversation_draft_tip">[草稿]</string>
<!-- message type-->
<string name="msg_type_location">[地理位置]</string>
<string name="msg_type_rtc_video">[視訊通話]</string>
<string name="msg_type_rtc_audio">[音訊通話]</string>
<string name="msg_type_image">[圖片訊息]</string>
<string name="msg_type_audio">[語音訊息]</string>
<string name="msg_type_video">[視訊訊息]</string>
<string name="msg_type_file">[檔案訊息]</string>
<string name="msg_type_notification">[通知訊息]</string>
<string name="msg_type_notification_announcement">[群公告] %s</string>
<string name="msg_type_tip">[提醒訊息]</string>
<string name="msg_type_custom">[自訂訊息]</string>
<string name="msg_type_no_tips">[未知訊息體]</string>
<string name="fun_conversation_search_text">搜尋</string>
<string name="time_zuotian_txt">昨天</string>
<string name="mine_text">我的</string>
<string name="chat_message_revoke_content">撤回了一則訊息</string>
<string name="services_text">客服</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<resources>
<style name="ConversationSelectorRadioBtn" parent="Widget.AppCompat.CompoundButton.RadioButton">
<item name="android:button">@drawable/conversation_radio_button_selector</item>
</style>
</resources>

View File

@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2022 NetEase, Inc. All rights reserved.
~ Use of this source code is governed by a MIT license that can be
~ found in the LICENSE file.
-->
<PopoEmoticons>
<Catalog Title="default">
<Emoticon File="emoji_00.png" ID="emoticon_emoji_0" Tag="[大笑]" />
<Emoticon File="emoji_01.png" ID="emoticon_emoji_01" Tag="[开心]" />
<Emoticon File="emoji_02.png" ID="emoticon_emoji_02" Tag="[色]" />
<Emoticon File="emoji_03.png" ID="emoticon_emoji_03" Tag="[酷]" />
<Emoticon File="emoji_04.png" ID="emoticon_emoji_04" Tag="[奸笑]" />
<Emoticon File="emoji_05.png" ID="emoticon_emoji_05" Tag="[亲]" />
<Emoticon File="emoji_06.png" ID="emoticon_emoji_06" Tag="[伸舌头]" />
<Emoticon File="emoji_07.png" ID="emoticon_emoji_07" Tag="[眯眼]" />
<Emoticon File="emoji_08.png" ID="emoticon_emoji_08" Tag="[可爱]" />
<Emoticon File="emoji_09.png" ID="emoticon_emoji_09" Tag="[鬼脸]" />
<Emoticon File="emoji_10.png" ID="emoticon_emoji_10" Tag="[偷笑]" />
<Emoticon File="emoji_11.png" ID="emoticon_emoji_11" Tag="[喜悦]" />
<Emoticon File="emoji_12.png" ID="emoticon_emoji_12" Tag="[狂喜]" />
<Emoticon File="emoji_13.png" ID="emoticon_emoji_13" Tag="[惊讶]" />
<Emoticon File="emoji_14.png" ID="emoticon_emoji_14" Tag="[流泪]" />
<Emoticon File="emoji_15.png" ID="emoticon_emoji_15" Tag="[流汗]" />
<Emoticon File="emoji_16.png" ID="emoticon_emoji_16" Tag="[天使]" />
<Emoticon File="emoji_17.png" ID="emoticon_emoji_17" Tag="[笑哭]" />
<Emoticon File="emoji_18.png" ID="emoticon_emoji_18" Tag="[尴尬]" />
<Emoticon File="emoji_19.png" ID="emoticon_emoji_19" Tag="[惊恐]" />
<Emoticon File="emoji_20.png" ID="emoticon_emoji_20" Tag="[大哭]" />
<Emoticon File="emoji_21.png" ID="emoticon_emoji_21" Tag="[烦躁]" />
<Emoticon File="emoji_22.png" ID="emoticon_emoji_22" Tag="[恐怖]" />
<Emoticon File="emoji_23.png" ID="emoticon_emoji_23" Tag="[两眼冒星]" />
<Emoticon File="emoji_24.png" ID="emoticon_emoji_24" Tag="[害羞]" />
<Emoticon File="emoji_25.png" ID="emoticon_emoji_25" Tag="[睡着]" />
<Emoticon File="emoji_26.png" ID="emoticon_emoji_26" Tag="[冒星]" />
<Emoticon File="emoji_27.png" ID="emoticon_emoji_27" Tag="[口罩]" />
<Emoticon File="emoji_28.png" ID="emoticon_emoji_28" Tag="[OK]" />
<Emoticon File="emoji_29.png" ID="emoticon_emoji_29" Tag="[好吧]" />
<Emoticon File="emoji_30.png" ID="emoticon_emoji_30" Tag="[鄙视]" />
<Emoticon File="emoji_31.png" ID="emoticon_emoji_31" Tag="[难受]" />
<Emoticon File="emoji_32.png" ID="emoticon_emoji_32" Tag="[不屑]" />
<Emoticon File="emoji_33.png" ID="emoticon_emoji_33" Tag="[不舒服]" />
<Emoticon File="emoji_34.png" ID="emoticon_emoji_34" Tag="[愤怒]" />
<Emoticon File="emoji_35.png" ID="emoticon_emoji_35" Tag="[鬼怪]" />
<Emoticon File="emoji_36.png" ID="emoticon_emoji_36" Tag="[发怒]" />
<Emoticon File="emoji_37.png" ID="emoticon_emoji_37" Tag="[生气]" />
<Emoticon File="emoji_38.png" ID="emoticon_emoji_38" Tag="[不高兴]" />
<Emoticon File="emoji_39.png" ID="emoticon_emoji_39" Tag="[皱眉]" />
<Emoticon File="emoji_40.png" ID="emoticon_emoji_40" Tag="[心碎]" />
<Emoticon File="emoji_41.png" ID="emoticon_emoji_41" Tag="[心动]" />
<Emoticon File="emoji_42.png" ID="emoticon_emoji_42" Tag="[好的]" />
<Emoticon File="emoji_43.png" ID="emoticon_emoji_43" Tag="[低级]" />
<Emoticon File="emoji_44.png" ID="emoticon_emoji_44" Tag="[赞]" />
<Emoticon File="emoji_45.png" ID="emoticon_emoji_45" Tag="[鼓掌]" />
<Emoticon File="emoji_46.png" ID="emoticon_emoji_46" Tag="[给力]" />
<Emoticon File="emoji_47.png" ID="emoticon_emoji_47" Tag="[打你]" />
<Emoticon File="emoji_48.png" ID="emoticon_emoji_48" Tag="[阿弥陀佛]" />
<Emoticon File="emoji_49.png" ID="emoticon_emoji_49" Tag="[拜拜]" />
<Emoticon File="emoji_50.png" ID="emoticon_emoji_50" Tag="[第一]" />
<Emoticon File="emoji_51.png" ID="emoticon_emoji_51" Tag="[拳头]" />
<Emoticon File="emoji_52.png" ID="emoticon_emoji_52" Tag="[手掌]" />
<Emoticon File="emoji_53.png" ID="emoticon_emoji_53" Tag="[剪刀]" />
<Emoticon File="emoji_54.png" ID="emoticon_emoji_54" Tag="[招手]" />
<Emoticon File="emoji_55.png" ID="emoticon_emoji_55" Tag="[不要]" />
<Emoticon File="emoji_56.png" ID="emoticon_emoji_56" Tag="[举着]" />
<Emoticon File="emoji_57.png" ID="emoticon_emoji_57" Tag="[思考]" />
<Emoticon File="emoji_58.png" ID="emoticon_emoji_58" Tag="[猪头]" />
<Emoticon File="emoji_59.png" ID="emoticon_emoji_59" Tag="[不听]" />
<Emoticon File="emoji_60.png" ID="emoticon_emoji_60" Tag="[不看]" />
<Emoticon File="emoji_61.png" ID="emoticon_emoji_61" Tag="[不说]" />
<Emoticon File="emoji_62.png" ID="emoticon_emoji_62" Tag="[猴子]" />
<Emoticon File="emoji_63.png" ID="emoticon_emoji_63" Tag="[炸弹]" />
<Emoticon File="emoji_64.png" ID="emoticon_emoji_64" Tag="[睡觉]" />
<Emoticon File="emoji_65.png" ID="emoticon_emoji_65" Tag="[筋斗云]" />
<Emoticon File="emoji_66.png" ID="emoticon_emoji_66" Tag="[火箭]" />
<Emoticon File="emoji_67.png" ID="emoticon_emoji_67" Tag="[救护车]" />
<Emoticon File="emoji_68.png" ID="emoticon_emoji_68" Tag="[便便]" />
</Catalog>
<Catalog Title="tingting">
<Emoticon File="tingting_00.png" ID="emoticon_tingting_00" Tag="[町町安靜]" />
<Emoticon File="tingting_01.png" ID="emoticon_emoji_01" Tag="[町町無辜]" />
<Emoticon File="tingting_02.png" ID="emoticon_tingting_02" Tag="[町町白眼]" />
<Emoticon File="tingting_03.png" ID="emoticon_tingting_03" Tag="[町町生氣]" />
<Emoticon File="tingting_04.png" ID="emoticon_tingting_04" Tag="[町町再見]" />
<Emoticon File="tingting_05.png" ID="emoticon_tingting_05" Tag="[町町閉嘴]" />
<Emoticon File="tingting_06.png" ID="emoticon_tingting_06" Tag="[町町犯困]" />
<Emoticon File="tingting_07.png" ID="emoticon_tingting_07" Tag="[町町尷尬]" />
<Emoticon File="tingting_08.png" ID="emoticon_tingting_08" Tag="[町町感動]" />
<Emoticon File="tingting_09.png" ID="emoticon_tingting_09" Tag="[町町鼓掌]" />
<Emoticon File="tingting_10.png" ID="emoticon_tingting_10" Tag="[町町害羞]" />
<Emoticon File="tingting_11.png" ID="emoticon_tingting_11" Tag="[町町好色]" />
<Emoticon File="tingting_12.png" ID="emoticon_tingting_12" Tag="[町町黑臉]" />
<Emoticon File="tingting_13.png" ID="emoticon_tingting_13" Tag="[町町急怒]" />
<Emoticon File="tingting_14.png" ID="emoticon_tingting_14" Tag="[町町驚慌]" />
<Emoticon File="tingting_15.png" ID="emoticon_tingting_15" Tag="[町町驚訝]" />
<Emoticon File="tingting_16.png" ID="emoticon_tingting_16" Tag="[町町沮喪]" />
<Emoticon File="tingting_17.png" ID="emoticon_tingting_17" Tag="[町町開心]" />
<Emoticon File="tingting_18.png" ID="emoticon_tingting_18" Tag="[町町瞌睡]" />
<Emoticon File="tingting_19.png" ID="emoticon_tingting_19" Tag="[町町摳鼻]" />
<Emoticon File="tingting_20.png" ID="emoticon_tingting_20" Tag="[町町哭泣]" />
<Emoticon File="tingting_21.png" ID="emoticon_tingting_21" Tag="[町町苦臉]" />
<Emoticon File="tingting_22.png" ID="emoticon_tingting_22" Tag="[町町酷]" />
<Emoticon File="tingting_23.png" ID="emoticon_tingting_23" Tag="[町町 酷]" />
<Emoticon File="tingting_24.png" ID="emoticon_tingting_24" Tag="[町町狂躁]" />
<Emoticon File="tingting_25.png" ID="emoticon_tingting_25" Tag="[町町流汗]" />
<Emoticon File="tingting_26.png" ID="emoticon_tingting_26" Tag="[町町罵人]" />
<Emoticon File="tingting_27.png" ID="emoticon_tingting_27" Tag="[町町發怒]" />
<Emoticon File="tingting_28.png" ID="emoticon_tingting_28" Tag="[町町氣哭]" />
<Emoticon File="tingting_29.png" ID="emoticon_tingting_29" Tag="[町町敲頭]" />
<Emoticon File="tingting_30.png" ID="emoticon_tingting_30" Tag="[町町親親]" />
<Emoticon File="tingting_31.png" ID="emoticon_tingting_31" Tag="[町町吐舌頭]" />
<Emoticon File="tingting_32.png" ID="emoticon_tingting_32" Tag="[町町微笑]" />
<Emoticon File="tingting_33.png" ID="emoticon_tingting_33" Tag="[町町捂嘴笑]" />
<Emoticon File="tingting_34.png" ID="emoticon_tingting_34" Tag="[町町兇]" />
<Emoticon File="tingting_35.png" ID="emoticon_tingting_35" Tag="[町町羞笑]" />
<Emoticon File="tingting_36.png" ID="emoticon_tingting_36" Tag="[町町眩暈]" />
<Emoticon File="tingting_37.png" ID="emoticon_tingting_37" Tag="[町町疑問]" />
<Emoticon File="tingting_38.png" ID="emoticon_tingting_38" Tag="[町町中毒]" />
<Emoticon File="tingting_39.png" ID="emoticon_tingting_39" Tag="[町町齜牙]" />
</Catalog>
<Catalog Title="taotao">
<Emoticon File="taotao_00.png" ID="emoticon_taotao_00" Tag="[淘淘安靜]" />
<Emoticon File="taotao_01.png" ID="emoticon_taotao_01" Tag="[淘淘白眼]" />
<Emoticon File="taotao_02.png" ID="emoticon_taotao_02" Tag="[淘淘生氣]" />
<Emoticon File="taotao_03.png" ID="emoticon_taotao_03" Tag="[淘淘閉嘴]" />
<Emoticon File="taotao_04.png" ID="emoticon_taotao_04" Tag="[淘淘犯困]" />
<Emoticon File="taotao_05.png" ID="emoticon_taotao_05" Tag="[淘淘尷尬]" />
<Emoticon File="taotao_06.png" ID="emoticon_taotao_06" Tag="[淘淘感動]" />
<Emoticon File="taotao_07.png" ID="emoticon_taotao_07" Tag="[淘淘鼓掌]" />
<Emoticon File="taotao_08.png" ID="emoticon_taotao_08" Tag="[淘淘害羞]" />
<Emoticon File="taotao_09.png" ID="emoticon_taotao_09" Tag="[淘淘好色]" />
<Emoticon File="taotao_10.png" ID="emoticon_taotao_10" Tag="[淘淘黑臉]" />
<Emoticon File="taotao_11.png" ID="emoticon_taotao_11" Tag="[淘淘急怒]" />
<Emoticon File="taotao_12.png" ID="emoticon_taotao_12" Tag="[淘淘驚慌]" />
<Emoticon File="taotao_13.png" ID="emoticon_taotao_13" Tag="[淘淘驚訝]" />
<Emoticon File="taotao_14.png" ID="emoticon_taotao_14" Tag="[淘淘沮喪]" />
<Emoticon File="taotao_15.png" ID="emoticon_taotao_15" Tag="[淘淘開心]" />
<Emoticon File="taotao_16.png" ID="emoticon_taotao_16" Tag="[淘淘瞌睡]" />
<Emoticon File="taotao_17.png" ID="emoticon_taotao_17" Tag="[淘淘摳鼻]" />
<Emoticon File="taotao_18.png" ID="emoticon_taotao_18" Tag="[淘淘哭泣]" />
<Emoticon File="taotao_19.png" ID="emoticon_taotao_19" Tag="[淘淘苦臉]" />
<Emoticon File="taotao_20.png" ID="emoticon_taotao_20" Tag="[淘淘酷]" />
<Emoticon File="taotao_21.png" ID="emoticon_taotao_21" Tag="[淘淘 酷]" />
<Emoticon File="taotao_22.png" ID="emoticon_taotao_22" Tag="[淘淘狂躁]" />
<Emoticon File="taotao_23.png" ID="emoticon_taotao_23" Tag="[淘淘流汗]" />
<Emoticon File="taotao_24.png" ID="emoticon_taotao_24" Tag="[淘淘罵人]" />
<Emoticon File="taotao_25.png" ID="emoticon_taotao_25" Tag="[淘淘發怒]" />
<Emoticon File="taotao_26.png" ID="emoticon_taotao_26" Tag="[淘淘氣哭]" />
<Emoticon File="taotao_27.png" ID="emoticon_taotao_27" Tag="[淘淘敲頭]" />
<Emoticon File="taotao_28.png" ID="emoticon_taotao_28" Tag="[淘淘親親]" />
<Emoticon File="taotao_29.png" ID="emoticon_taotao_29" Tag="[淘淘吐舌頭]" />
<Emoticon File="taotao_30.png" ID="emoticon_taotao_30" Tag="[淘淘微笑]" />
<Emoticon File="taotao_31.png" ID="emoticon_taotao_31" Tag="[淘淘無辜]" />
<Emoticon File="taotao_32.png" ID="emoticon_taotao_32" Tag="[淘淘捂嘴笑]" />
<Emoticon File="taotao_33.png" ID="emoticon_taotao_33" Tag="[淘淘兇]" />
<Emoticon File="taotao_34.png" ID="emoticon_taotao_34" Tag="[淘淘羞笑]" />
<Emoticon File="taotao_35.png" ID="emoticon_taotao_35" Tag="[淘淘眩暈]" />
<Emoticon File="taotao_36.png" ID="emoticon_taotao_36" Tag="[淘淘疑問]" />
<Emoticon File="taotao_37.png" ID="emoticon_taotao_37" Tag="[淘淘再見]" />
<Emoticon File="taotao_38.png" ID="emoticon_taotao_38" Tag="[淘淘中毒]" />
<Emoticon File="taotao_39.png" ID="emoticon_taotao_39" Tag="[淘淘齜牙]" />
</Catalog>
<Catalog Title="lele">
<Emoticon File="lele_00.png" ID="emoticon_lele_00" Tag="[樂樂安靜]" />
<Emoticon File="lele_01.png" ID="emoticon_lele_01" Tag="[樂樂白眼]" />
<Emoticon File="lele_02.png" ID="emoticon_lele_02" Tag="[樂樂生氣]" />
<Emoticon File="lele_03.png" ID="emoticon_lele_37" Tag="[樂樂再見]" />
<Emoticon File="lele_04.png" ID="emoticon_lele_03" Tag="[樂樂閉嘴]" />
<Emoticon File="lele_05.png" ID="emoticon_lele_04" Tag="[樂樂犯困]" />
<Emoticon File="lele_06.png" ID="emoticon_lele_05" Tag="[樂樂尷尬]" />
<Emoticon File="lele_07.png" ID="emoticon_lele_06" Tag="[樂樂感動]" />
<Emoticon File="lele_08.png" ID="emoticon_lele_07" Tag="[樂樂鼓掌]" />
<Emoticon File="lele_09.png" ID="emoticon_lele_08" Tag="[樂樂害羞]" />
<Emoticon File="lele_10.png" ID="emoticon_lele_09" Tag="[樂樂好色]" />
<Emoticon File="lele_11.png" ID="emoticon_lele_10" Tag="[樂樂黑臉]" />
<Emoticon File="lele_12.png" ID="emoticon_lele_11" Tag="[樂樂急怒]" />
<Emoticon File="lele_13.png" ID="emoticon_lele_12" Tag="[樂樂驚慌]" />
<Emoticon File="lele_14.png" ID="emoticon_lele_13" Tag="[樂樂驚訝]" />
<Emoticon File="lele_15.png" ID="emoticon_lele_14" Tag="[樂樂沮喪]" />
<Emoticon File="lele_16.png" ID="emoticon_lele_15" Tag="[樂樂開心]" />
<Emoticon File="lele_17.png" ID="emoticon_lele_16" Tag="[樂樂瞌睡]" />
<Emoticon File="lele_18.png" ID="emoticon_lele_17" Tag="[樂樂摳鼻]" />
<Emoticon File="lele_19.png" ID="emoticon_lele_18" Tag="[樂樂哭泣]" />
<Emoticon File="lele_20.png" ID="emoticon_lele_19" Tag="[樂樂苦臉]" />
<Emoticon File="lele_21.png" ID="emoticon_lele_20" Tag="[樂樂酷]" />
<Emoticon File="lele_22.png" ID="emoticon_lele_21" Tag="[樂樂 酷]" />
<Emoticon File="lele_23.png" ID="emoticon_lele_22" Tag="[樂樂狂躁]" />
<Emoticon File="lele_24.png" ID="emoticon_lele_23" Tag="[樂樂流汗]" />
<Emoticon File="lele_25.png" ID="emoticon_lele_24" Tag="[樂樂罵人]" />
<Emoticon File="lele_26.png" ID="emoticon_lele_25" Tag="[樂樂發怒]" />
<Emoticon File="lele_27.png" ID="emoticon_lele_26" Tag="[樂樂氣哭]" />
<Emoticon File="lele_28.png" ID="emoticon_lele_27" Tag="[樂樂敲頭]" />
<Emoticon File="lele_29.png" ID="emoticon_lele_28" Tag="[樂樂親親]" />
<Emoticon File="lele_30.png" ID="emoticon_lele_29" Tag="[樂樂吐舌頭]" />
<Emoticon File="lele_31.png" ID="emoticon_lele_30" Tag="[樂樂微笑]" />
<Emoticon File="lele_32.png" ID="emoticon_lele_31" Tag="[樂樂無辜]" />
<Emoticon File="lele_33.png" ID="emoticon_lele_32" Tag="[樂樂捂嘴笑]" />
<Emoticon File="lele_34.png" ID="emoticon_lele_33" Tag="[樂樂兇]" />
<Emoticon File="lele_35.png" ID="emoticon_lele_34" Tag="[樂樂羞笑]" />
<Emoticon File="lele_36.png" ID="emoticon_lele_35" Tag="[樂樂眩暈]" />
<Emoticon File="lele_37.png" ID="emoticon_lele_36" Tag="[樂樂疑問]" />
<Emoticon File="lele_38.png" ID="emoticon_lele_38" Tag="[樂樂中毒]" />
<Emoticon File="lele_39.png" ID="emoticon_lele_39" Tag="[樂樂齜牙]" />
</Catalog>
<Catalog Title="tlt">
<Emoticon File="tlt_gif_00.png" ID="emoticon_tlt_gif_00" Tag="[好喜歡你哦]" />
<Emoticon File="tlt_gif_01.png" ID="emoticon_tlt_gif_01" Tag="[加油鴨]" />
<Emoticon File="tlt_gif_02.png" ID="emoticon_tlt_gif_02" Tag="[老大駕到]" />
<Emoticon File="tlt_gif_03.png" ID="emoticon_tlt_gif_03" Tag="[老妹你真美]" />
<Emoticon File="tlt_gif_04.png" ID="emoticon_tlt_gif_04" Tag="[靈魂抓手]" />
<Emoticon File="tlt_gif_05.png" ID="emoticon_tlt_gif_05" Tag="[菩薩保佑]" />
<Emoticon File="tlt_gif_06.png" ID="emoticon_tlt_gif_06" Tag="[氣屎我了]" />
<Emoticon File="tlt_gif_07.png" ID="emoticon_tlt_gif_07" Tag="[什麼情況]" />
<Emoticon File="tlt_gif_08.png" ID="emoticon_tlt_gif_08" Tag="[送你好運]" />
<Emoticon File="tlt_gif_09.png" ID="emoticon_tlt_gif_09" Tag="[太感動了]" />
<Emoticon File="tlt_gif_10.png" ID="emoticon_tlt_gif_10" Tag="[為愛而戰]" />
<Emoticon File="tlt_gif_11.png" ID="emoticon_tlt_gif_11" Tag="[要得手了]" />
<Emoticon File="tlt_gif_12.png" ID="emoticon_tlt_gif_12" Tag="[勇冠三軍]" />
<Emoticon File="tlt_gif_13.png" ID="emoticon_tlt_gif_13" Tag="[有驚喜哦]" />
<Emoticon File="tlt_gif_14.png" ID="emoticon_tlt_gif_14" Tag="[抓不著就搗蛋]" />
<Emoticon File="tlt_gif_15.png" ID="emoticon_tlt_gif_15" Tag="[聊天結束]" />
<Emoticon File="tlt_gif_16.png" ID="emoticon_tlt_gif_16" Tag="[蒼天啊]" />
<Emoticon File="tlt_gif_17.png" ID="emoticon_tlt_gif_17" Tag="[暈死]" />
<Emoticon File="tlt_gif_18.png" ID="emoticon_tlt_gif_18" Tag="[幹就完了]" />
<Emoticon File="tlt_gif_19.png" ID="emoticon_tlt_gif_19" Tag="[發財了]" />
<Emoticon File="tlt_gif_20.png" ID="emoticon_tlt_gif_20" Tag="[對不起]" />
<Emoticon File="tlt_gif_21.png" ID="emoticon_tlt_gif_21" Tag="[關我屁事]" />
<Emoticon File="tlt_gif_22.png" ID="emoticon_tlt_gif_22" Tag="[嚇死我了]" />
<Emoticon File="tlt_gif_23.png" ID="emoticon_tlt_gif_23" Tag="[送S去咯]" />
<Emoticon File="tlt_gif_24.png" ID="emoticon_tlt_gif_24" Tag="[洗洗睡吧]" />
<Emoticon File="tlt_gif_25.png" ID="emoticon_tlt_gif_25" Tag="[我要滅了你]" />
<Emoticon File="tlt_gif_26.png" ID="emoticon_tlt_gif_26" Tag="[電不倒你]" />
<Emoticon File="tlt_gif_27.png" ID="emoticon_tlt_gif_27" Tag="[大過年的]" />
<Emoticon File="tlt_gif_28.png" ID="emoticon_tlt_gif_28" Tag="[有人放屁]" />
<Emoticon File="tlt_gif_29.png" ID="emoticon_tlt_gif_29" Tag="[好樣的]" />
<Emoticon File="tlt_gif_30.png" ID="emoticon_tlt_gif_30" Tag="[哇噢]" />
<Emoticon File="tlt_gif_31.png" ID="emoticon_tlt_gif_31" Tag="[同志們好]" />
<Emoticon File="tlt_gif_32.png" ID="emoticon_tlt_gif_32" Tag="[沒問題]" />
<Emoticon File="tlt_gif_33.png" ID="emoticon_tlt_gif_33" Tag="[怎麼辦]" />
<Emoticon File="tlt_gif_34.png" ID="emoticon_tlt_gif_34" Tag="[偷瞄一下]" />
<Emoticon File="tlt_gif_35.png" ID="emoticon_tlt_gif_35" Tag="[今天舔誰呢]" />
</Catalog>
</PopoEmoticons>