feat : module_public_chat

This commit is contained in:
eggmanQQQ2
2025-07-07 10:34:58 +08:00
parent 424b73ce99
commit f9fa3e1e8c
36 changed files with 4526 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
package com.chwl.app.public_chat.core
import com.alibaba.fastjson2.JSON
import com.chwl.core.decoration.headwear.bean.HeadWearInfo
import com.chwl.core.super_admin.util.SuperAdminUtil
import com.chwl.core.user.UserModel
import com.chwl.core.user.bean.UserInfo
import com.chwl.core.utils.ExtensionUtils
import com.chwl.core.utils.net.RxHelper
import com.chwl.library.common.util.doLog
import com.example.lib_utils.ICleared
import com.example.lib_utils.log.ILog
import com.netease.nim.uikit.api.model.NimException
import com.netease.nimlib.sdk.NIMChatRoomSDK
import com.netease.nimlib.sdk.NIMClient
import com.netease.nimlib.sdk.Observer
import com.netease.nimlib.sdk.RequestCallback
import com.netease.nimlib.sdk.StatusCode
import com.netease.nimlib.sdk.chatroom.ChatRoomService
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
import com.netease.nimlib.sdk.chatroom.model.ChatRoomStatusChangeData
import com.netease.nimlib.sdk.chatroom.model.EnterChatRoomData
import com.netease.nimlib.sdk.chatroom.model.EnterChatRoomResultData
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum
import com.netease.nimlib.sdk.msg.model.QueryDirectionEnum
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
open class ChatRoomClient(val sessionId: String) : ICleared, ILog {
private val clientScope = MainScope()
private var isLogin = false
private var loginData: EnterChatRoomResultData? = null
private val messagePublishSubject: PublishSubject<List<ChatRoomMessage>> =
PublishSubject.create()
val messageObservable: Observable<List<ChatRoomMessage>> = messagePublishSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
val stateFlow = MutableStateFlow<StatusCode>(StatusCode.INVALID)
var messageFilter: ChatRoomMessageFilter? = null
private val receiveMessageObserver = Observer<List<ChatRoomMessage>> {
logD("receiveMessageObserver size:${it.size}")
val list = it.filter { item ->
item.sessionId == sessionId && (messageFilter?.filter(item) ?: true)
}
messagePublishSubject.onNext(list)
}
private val onlineStatusObserver = Observer<ChatRoomStatusChangeData> {
if (it.roomId != sessionId) {
return@Observer
}
if (it.status == StatusCode.LOGINED) {
isLogin = true
} else {
isLogin = false
}
clientScope.launch {
stateFlow.emit(it.status)
}
}
init {
registerOnlineStatus(true)
registerReceiveMessage(true)
}
fun enterChatRoom(): Single<EnterChatRoomResultData> {
logD("enterChatRoom() sessionId:$sessionId")
if (loginData != null && isLogin) {
logD("enterChatRoom() isLogin")
return Single.just(loginData)
}
return Single.create<EnterChatRoomResultData?> {
logD("enterChatRoom() sessionId:$sessionId run")
val enterChatRoomData = EnterChatRoomData(sessionId)
val userInfo = UserModel.get().cacheLoginUserInfo
if (userInfo != null) {
val nobleInfo = userInfo.nobleInfo
val headWearInfo = userInfo.userHeadwear
val userLevelVo = userInfo.userLevelVo
val carInfo = userInfo.carInfo
val map: MutableMap<String, Any> = java.util.HashMap(1)
var valueMap = userInfo.toMap(null, userInfo)
if (userLevelVo != null) {
valueMap = userLevelVo.toMap(
if (nobleInfo != null && nobleInfo.level > 0) nobleInfo.toMap(valueMap) else valueMap
)
valueMap["experLevelSeq"] = userLevelVo.getExperLevelSeq()
}
if (carInfo != null) {
valueMap = carInfo.toMap(valueMap, carInfo)
}
valueMap[UserInfo.NAMEPLATE_WORD] = userInfo.nameplateWord
valueMap[UserInfo.NAMEPLATE_PIC] = userInfo.nameplatePic
//多个判断头饰不过期才传pic
if (headWearInfo != null && headWearInfo.status == HeadWearInfo.STATUS_IN_USED) {
valueMap = headWearInfo.toMap(valueMap)
}
valueMap[SuperAdminUtil.PLATFORM_ROLE] = userInfo.platformRole
if (valueMap.isNotEmpty()) {
map[userInfo.uid.toString()] = valueMap
}
if (map.isNotEmpty()) {
enterChatRoomData.extension = map
}
}
val enterChatRoomEx =
NIMChatRoomSDK.getChatRoomService().enterChatRoomEx(enterChatRoomData, 3)
enterChatRoomEx.setCallback(object : RequestCallback<EnterChatRoomResultData> {
override fun onSuccess(param: EnterChatRoomResultData) {
logD("enterChatRoom() onSuccess roomId:${param.roomId}")
loginData = param
it.onSuccess(param)
}
override fun onFailed(code: Int) {
logD("enterChatRoom() onFailed code:${code}")
it.onError(NimException(code))
}
override fun onException(exception: Throwable) {
logD("enterChatRoom() onException exception:${exception}")
exception.printStackTrace()
it.onError(exception)
}
})
}.compose(RxHelper.handleSchedulers())
}
fun exitChatRoom() {
logD("exitChatRoom() sessionId:$sessionId")
NIMChatRoomSDK.getChatRoomService().exitChatRoom(sessionId)
}
fun sendMessage(message: ChatRoomMessage): Single<Any> {
val map: Map<String?, Any?> = message.localExtension ?: HashMap()
val userInfo = UserModel.get().cacheLoginUserInfo
if (userInfo != null) {
ExtensionUtils.setupExtension(map, userInfo)
}
message.localExtension = map
message.remoteExtension = map
return Single.create<Any> {
logD("sendMessage() message:${message}")
"发消息时 携带的用户信息 UserMap = $map".doLog()
"发消息时 消息实体 = ${JSON.toJSONString(message)}".doLog()
NIMClient.getService<ChatRoomService>(ChatRoomService::class.java)
.sendMessage(message, false)
.setCallback(object : RequestCallback<Void?> {
override fun onSuccess(param: Void?) {
logD("sendMessage() onSuccess")
it.onSuccess(Any())
}
override fun onFailed(code: Int) {
logD("sendMessage() onFailed code:$code")
it.onError(NimException(code))
}
override fun onException(exception: Throwable) {
logD("sendMessage() onException exception:$exception")
exception.printStackTrace()
it.onError(exception)
}
})
}.compose(RxHelper.handleSchedulers())
}
private fun registerReceiveMessage(register: Boolean) {
NIMChatRoomSDK.getChatRoomServiceObserve()
.observeReceiveMessage(receiveMessageObserver, register)
}
private fun registerOnlineStatus(register: Boolean) {
NIMChatRoomSDK.getChatRoomServiceObserve()
.observeOnlineStatus(onlineStatusObserver, register)
}
fun requestRemoteMessageType(
startTime: Long,
count: Int,
direction: QueryDirectionEnum,
typeEnums: Array<MsgTypeEnum>
): Single<List<ChatRoomMessage>> {
return Single.create<List<ChatRoomMessage>> {
NIMClient.getService<ChatRoomService>(ChatRoomService::class.java)
.pullMessageHistoryExType(
sessionId,
startTime,
count,
direction,
typeEnums
).setCallback(object : RequestCallback<List<ChatRoomMessage>> {
override fun onSuccess(param: List<ChatRoomMessage>?) {
it.onSuccess(param ?: emptyList())
}
override fun onFailed(code: Int) {
it.onError(NimException(code))
}
override fun onException(exception: Throwable) {
it.onError(exception)
}
})
}.compose(RxHelper.handleSchedulers())
}
override fun onCleared() {
super.onCleared()
ChatRoomClientManager.onClientCleared(this)
registerReceiveMessage(false)
registerOnlineStatus(false)
exitChatRoom()
try {
clientScope.cancel()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,58 @@
package com.chwl.app.public_chat.core
import com.chwl.core.im.custom.bean.CustomAttachment
import com.chwl.core.im.custom.bean.HeadlineChangedAttachment
import com.chwl.core.initial.InitialModel
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum
object ChatRoomClientManager {
private val clients = HashMap<String, ChatRoomClient>()
val publicChatRoomReceiveMessageFilter: ChatRoomMessageFilter = object : ChatRoomMessageFilter {
override fun filter(message: ChatRoomMessage): Boolean {
if (message.msgType == MsgTypeEnum.image || message.msgType == MsgTypeEnum.text) {
return true
}
if (message.msgType == MsgTypeEnum.custom) {
val attachment: CustomAttachment =
(message.attachment as? CustomAttachment) ?: return true
when (attachment.first) {
CustomAttachment.CUSTOM_MSG_HEADLINE_CHANGED -> {
when (attachment.second) {
CustomAttachment.CUSTOM_MSG_HEADLINE_CHANGED_SUB -> {
return true
}
}
}
}
}
return false
}
}
fun getPublicChatClient(): ChatRoomClient? {
val sessionId = InitialModel.get().publicChatSessionId
if (sessionId.isNullOrEmpty()) {
return null
}
return getClient(sessionId).apply {
this.messageFilter = publicChatRoomReceiveMessageFilter
}
}
fun getClient(sessionId: String): ChatRoomClient {
var finalClient = clients[sessionId]
if (finalClient?.sessionId == sessionId) {
return finalClient
}
finalClient = ChatRoomClient(sessionId)
clients[sessionId] = finalClient
return finalClient
}
fun onClientCleared(client: ChatRoomClient) {
clients.remove(client.sessionId)
}
}

View File

@@ -0,0 +1,262 @@
package com.chwl.app.public_chat.core;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.chwl.app.public_chat.core.viewholder.ChatRoomMessageViewHolderBase;
import com.chwl.app.public_chat.core.viewholder.ChatRoomMessageViewHolderFactory;
import com.chwl.library.utils.LogUtil;
import com.chwl.library.widget.SVGAView;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.business.session.module.Container;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
import com.netease.nim.uikit.common.ui.recyclerview.holder.PublicChatRoomNimBaseViewHolder;
import com.netease.nim.uikit.impl.NimUIKitImpl;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
import com.netease.nimlib.sdk.msg.model.IMMessage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Created by huangjun on 2016/12/21.
*/
public class ChatRoomMessageAdapter extends BaseMultiItemFetchLoadAdapter<ChatRoomMessage, PublicChatRoomNimBaseViewHolder> {
private Map<Class<? extends ChatRoomMessageViewHolderBase>, Integer> holder2ViewType;
private ViewHolderEventListener eventListener;
private Map<String, Float> progresses; // 有文件传输需要显示进度条的消息ID map
private String messageId;
private Container container;
private SVGAView.SVGACache svgaCache = SVGAView.newCache(20);
public ChatRoomMessageAdapter(RecyclerView recyclerView, List<ChatRoomMessage> data, Container container) {
super(recyclerView, data);
timedItems = new HashSet<>();
progresses = new HashMap<>();
// view type, view holder
holder2ViewType = new HashMap<>();
List<Class<? extends ChatRoomMessageViewHolderBase>> holders = ChatRoomMessageViewHolderFactory.getAllViewHolders();
int viewType = 0;
for (Class<? extends ChatRoomMessageViewHolderBase> holder : holders) {
viewType++;
addItemType(viewType, R.layout.nim_chat_room_message_item, holder);
holder2ViewType.put(holder, viewType);
}
this.container = container;
}
@Override
protected PublicChatRoomNimBaseViewHolder createBaseViewHolder(View view) {
return new PublicChatRoomNimBaseViewHolder(view, svgaCache);
}
@Override
protected int getViewType(ChatRoomMessage message) {
try {
Class<? extends ChatRoomMessageViewHolderBase> clazz = (ChatRoomMessageViewHolderFactory.getViewHolderByType(message));
LogUtil.print("MsgViewHolder=" + clazz.getSimpleName() + " --- MessageType=" + message.getMsgType().name());
return holder2ViewType.get(clazz);
} catch (Exception e) {
return 0;
}
}
@Override
protected String getItemKey(ChatRoomMessage item) {
return item.getUuid();
}
public void setEventListener(ViewHolderEventListener eventListener) {
this.eventListener = eventListener;
}
public ViewHolderEventListener getEventListener() {
return eventListener;
}
public void deleteItem(IMMessage message, boolean isRelocateTime) {
if (message == null) {
return;
}
int index = 0;
for (IMMessage item : getData()) {
if (item.isTheSame(message)) {
break;
}
++index;
}
if (index < getDataSize()) {
remove(index);
if (isRelocateTime) {
relocateShowTimeItemAfterDelete(message, index);
}
notifyDataSetChanged(); // 可以不要!!!
}
}
public float getProgress(IMMessage message) {
Float progress = progresses.get(message.getUuid());
return progress == null ? 0 : progress;
}
public void putProgress(IMMessage message, float progress) {
progresses.put(message.getUuid(), progress);
}
/**
* *********************** 时间显示处理 ***********************
*/
private Set<String> timedItems; // 需要显示消息时间的消息ID
private IMMessage lastShowTimeItem; // 用于消息时间显示,判断和上条消息间的时间间隔
public boolean needShowTime(IMMessage message) {
return timedItems.contains(message.getUuid());
}
/**
* 列表加入新消息时,更新时间显示
*/
public void updateShowTimeItem(List<ChatRoomMessage> items, boolean fromStart, boolean update) {
IMMessage anchor = fromStart ? null : lastShowTimeItem;
for (IMMessage message : items) {
if (setShowTimeFlag(message, anchor)) {
anchor = message;
}
}
if (update) {
lastShowTimeItem = anchor;
}
}
/**
* 是否显示时间item
*/
private boolean setShowTimeFlag(IMMessage message, IMMessage anchor) {
boolean update = false;
if (hideTimeAlways(message)) {
setShowTime(message, false);
} else {
if (anchor == null) {
setShowTime(message, true);
update = true;
} else {
long time = anchor.getTime();
long now = message.getTime();
if (now - time == 0) {
// 消息撤回时使用
setShowTime(message, true);
lastShowTimeItem = message;
update = true;
} else if (now - time < (NimUIKitImpl.getOptions().displayMsgTimeWithInterval)) {
setShowTime(message, false);
} else {
setShowTime(message, true);
update = true;
}
}
}
return update;
}
private void setShowTime(IMMessage message, boolean show) {
if (show) {
timedItems.add(message.getUuid());
} else {
timedItems.remove(message.getUuid());
}
}
private void relocateShowTimeItemAfterDelete(IMMessage messageItem, int index) {
// 如果被删的项显示了时间,需要继承
if (needShowTime(messageItem)) {
setShowTime(messageItem, false);
if (getDataSize() > 0) {
IMMessage nextItem;
if (index == getDataSize()) {
//删除的是最后一项
nextItem = getItem(index - 1);
} else {
//删除的不是最后一项
nextItem = getItem(index);
}
// 增加其他不需要显示时间的消息类型判断
if (hideTimeAlways(nextItem)) {
setShowTime(nextItem, false);
if (lastShowTimeItem != null && lastShowTimeItem != null
&& lastShowTimeItem.isTheSame(messageItem)) {
lastShowTimeItem = null;
for (int i = getDataSize() - 1; i >= 0; i--) {
IMMessage item = getItem(i);
if (needShowTime(item)) {
lastShowTimeItem = item;
break;
}
}
}
} else {
setShowTime(nextItem, true);
if (lastShowTimeItem == null
|| (lastShowTimeItem != null && lastShowTimeItem.isTheSame(messageItem))) {
lastShowTimeItem = nextItem;
}
}
} else {
lastShowTimeItem = null;
}
}
}
private boolean hideTimeAlways(IMMessage message) {
// if (message.getSessionType() == SessionTypeEnum.ChatRoom) {
// return true;
// }
switch (message.getMsgType()) {
case notification:
return true;
default:
return false;
}
}
public interface ViewHolderEventListener {
// 长按事件响应处理
boolean onViewHolderLongClick(View clickView, View viewHolderView, IMMessage item);
// 发送失败或者多媒体文件下载失败指示按钮点击响应处理
void onFailedBtnClick(IMMessage resendMessage);
// viewholder footer按钮点击如机器人继续会话
void onFooterClick(IMMessage message);
}
public void setUuid(String messageId) {
this.messageId = messageId;
}
public String getUuid() {
return messageId;
}
public Container getContainer() {
return container;
}
}

View File

@@ -0,0 +1,7 @@
package com.chwl.app.public_chat.core
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
interface ChatRoomMessageFilter {
fun filter(message: ChatRoomMessage): Boolean
}

View File

@@ -0,0 +1,302 @@
package com.chwl.app.public_chat.core.viewholder;
import android.graphics.drawable.AnimationDrawable;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Keep;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.business.session.audio.MessageAudioControl;
import com.netease.nim.uikit.common.media.audioplayer.Playable;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
import com.netease.nim.uikit.common.util.sys.ScreenUtil;
import com.netease.nim.uikit.common.util.sys.TimeUtil;
import com.netease.nim.uikit.impl.NimUIKitImpl;
import com.netease.nimlib.sdk.msg.attachment.AudioAttachment;
import com.netease.nimlib.sdk.msg.constant.AttachStatusEnum;
import com.netease.nimlib.sdk.msg.constant.MsgDirectionEnum;
import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
import com.netease.nimlib.sdk.msg.model.IMMessage;
/**
* Created by zhoujianghua on 2015/8/5.
*/
@Keep
public class ChatRoomMessageViewHolderAudio extends ChatRoomMessageViewHolderBase {
public ChatRoomMessageViewHolderAudio(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
}
public static final int CLICK_TO_PLAY_AUDIO_DELAY = 500;
private TextView durationLabel;
private View containerView;
private View unreadIndicator;
private ImageView animationView;
private MessageAudioControl audioControl;
@Override
protected int getContentResId() {
return R.layout.nim_message_item_audio;
}
@Override
protected void inflateContentView() {
durationLabel = findViewById(R.id.message_item_audio_duration);
containerView = findViewById(R.id.message_item_audio_container);
unreadIndicator = findViewById(R.id.message_item_audio_unread_indicator);
animationView = findViewById(R.id.message_item_audio_playing_animation);
animationView.setBackgroundResource(0);
audioControl = MessageAudioControl.getInstance(context);
}
@Override
protected void bindContentView() {
layoutByDirection();
refreshStatus();
controlPlaying();
}
@Override
protected void onItemClick() {
if (audioControl != null) {
if (message.getDirect() == MsgDirectionEnum.In && message.getAttachStatus() != AttachStatusEnum.transferred) {
return;
}
if (message.getStatus() != MsgStatusEnum.read) {
// 将未读标识去掉,更新数据库
unreadIndicator.setVisibility(View.GONE);
}
initPlayAnim(); // 设置语音播放动画
audioControl.startPlayAudioDelay(CLICK_TO_PLAY_AUDIO_DELAY, message, onPlayListener);
audioControl.setPlayNext(!NimUIKitImpl.getOptions().disableAutoPlayNextAudio, adapter, message);
}
}
private void layoutByDirection() {
if (isReceivedMessage()) {
setGravity(animationView, Gravity.LEFT | Gravity.CENTER_VERTICAL);
setGravity(durationLabel, Gravity.RIGHT | Gravity.CENTER_VERTICAL);
containerView.setBackgroundResource(NimUIKitImpl.getOptions().messageLeftBackground);
containerView.setPadding(ScreenUtil.dip2px(15), ScreenUtil.dip2px(8), ScreenUtil.dip2px(10), ScreenUtil.dip2px(8));
animationView.setBackgroundResource(R.drawable.nim_audio_animation_list_left);
} else {
setGravity(animationView, Gravity.RIGHT | Gravity.CENTER_VERTICAL);
setGravity(durationLabel, Gravity.LEFT | Gravity.CENTER_VERTICAL);
unreadIndicator.setVisibility(View.GONE);
containerView.setBackgroundResource(NimUIKitImpl.getOptions().messageRightBackground);
containerView.setPadding(ScreenUtil.dip2px(10), ScreenUtil.dip2px(8), ScreenUtil.dip2px(15), ScreenUtil.dip2px(8));
animationView.setBackgroundResource(R.drawable.nim_audio_animation_list_right);
}
}
private void refreshStatus() {// 消息状态
AudioAttachment attachment = (AudioAttachment) message.getAttachment();
MsgStatusEnum status = message.getStatus();
AttachStatusEnum attachStatus = message.getAttachStatus();
// alert button
if (TextUtils.isEmpty(attachment.getPath())) {
if (attachStatus == AttachStatusEnum.fail || status == MsgStatusEnum.fail) {
alertButton.setVisibility(View.VISIBLE);
} else {
alertButton.setVisibility(View.GONE);
}
}
// progress bar indicator
if (status == MsgStatusEnum.sending || attachStatus == AttachStatusEnum.transferring) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
// unread indicator
if (!NimUIKitImpl.getOptions().disableAudioPlayedStatusIcon
&& isReceivedMessage()
&& attachStatus == AttachStatusEnum.transferred
&& status != MsgStatusEnum.read) {
unreadIndicator.setVisibility(View.VISIBLE);
} else {
unreadIndicator.setVisibility(View.GONE);
}
}
private void controlPlaying() {
final AudioAttachment msgAttachment = (AudioAttachment) message.getAttachment();
long duration = msgAttachment.getDuration();
setAudioBubbleWidth(duration);
durationLabel.setTag(message.getUuid());
if (!isMessagePlaying(audioControl, message)) {
if (audioControl.getAudioControlListener() != null &&
audioControl.getAudioControlListener().equals(onPlayListener)) {
audioControl.changeAudioControlListener(null);
}
updateTime(duration);
stop();
} else {
audioControl.changeAudioControlListener(onPlayListener);
play();
}
}
public static int getAudioMaxEdge() {
return (int) (0.6 * ScreenUtil.screenMin);
}
public static int getAudioMinEdge() {
return (int) (0.1875 * ScreenUtil.screenMin);
}
private void setAudioBubbleWidth(long milliseconds) {
long seconds = TimeUtil.getSecondsByMilliseconds(milliseconds);
int currentBubbleWidth = calculateBubbleWidth(seconds, NimUIKitImpl.getOptions().audioRecordMaxTime);
ViewGroup.LayoutParams layoutParams = containerView.getLayoutParams();
layoutParams.width = currentBubbleWidth;
containerView.setLayoutParams(layoutParams);
}
private int calculateBubbleWidth(long seconds, int MAX_TIME) {
int maxAudioBubbleWidth = getAudioMaxEdge();
int minAudioBubbleWidth = getAudioMinEdge();
int currentBubbleWidth;
if (seconds <= 0) {
currentBubbleWidth = minAudioBubbleWidth;
} else if (seconds > 0 && seconds <= MAX_TIME) {
currentBubbleWidth = (int) ((maxAudioBubbleWidth - minAudioBubbleWidth) * (2.0 / Math.PI)
* Math.atan(seconds / 10.0) + minAudioBubbleWidth);
} else {
currentBubbleWidth = maxAudioBubbleWidth;
}
if (currentBubbleWidth < minAudioBubbleWidth) {
currentBubbleWidth = minAudioBubbleWidth;
} else if (currentBubbleWidth > maxAudioBubbleWidth) {
currentBubbleWidth = maxAudioBubbleWidth;
}
return currentBubbleWidth;
}
private void updateTime(long milliseconds) {
long seconds = TimeUtil.getSecondsByMilliseconds(milliseconds);
if (seconds >= 0) {
durationLabel.setText(seconds + "\"");
} else {
durationLabel.setText("");
}
}
protected boolean isMessagePlaying(MessageAudioControl audioControl, IMMessage message) {
if (audioControl.getPlayingAudio() != null && audioControl.getPlayingAudio().isTheSame(message)) {
return true;
} else {
return false;
}
}
private MessageAudioControl.AudioControlListener onPlayListener = new MessageAudioControl.AudioControlListener() {
@Override
public void updatePlayingProgress(Playable playable, long curPosition) {
if (!isTheSame(message.getUuid())) {
return;
}
if (curPosition > playable.getDuration()) {
return;
}
updateTime(curPosition);
}
@Override
public void onAudioControllerReady(Playable playable) {
if (!isTheSame(message.getUuid())) {
return;
}
play();
}
@Override
public void onEndPlay(Playable playable) {
if (!isTheSame(message.getUuid())) {
return;
}
updateTime(playable.getDuration());
stop();
}
};
private void play() {
if (animationView.getBackground() instanceof AnimationDrawable) {
AnimationDrawable animation = (AnimationDrawable) animationView.getBackground();
animation.start();
}
}
private void stop() {
if (animationView.getBackground() instanceof AnimationDrawable) {
AnimationDrawable animation = (AnimationDrawable) animationView.getBackground();
animation.stop();
endPlayAnim();
}
}
private void initPlayAnim() {
if (isReceivedMessage()) {
animationView.setBackgroundResource(R.drawable.nim_audio_animation_list_left);
} else {
animationView.setBackgroundResource(R.drawable.nim_audio_animation_list_right);
}
}
private void endPlayAnim() {
if (isReceivedMessage()) {
animationView.setBackgroundResource(R.drawable.nim_audio_animation_list_left_3);
} else {
animationView.setBackgroundResource(R.drawable.nim_audio_animation_list_right_3);
}
}
private boolean isTheSame(String uuid) {
String current = durationLabel.getTag().toString();
return !TextUtils.isEmpty(uuid) && uuid.equals(current);
}
@Override
protected int leftBackground() {
return 0;
}
@Override
protected int rightBackground() {
return 0;
}
}

View File

@@ -0,0 +1,490 @@
package com.chwl.app.public_chat.core.viewholder;
import android.content.Context;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import com.chwl.app.public_chat.core.ChatRoomMessageAdapter;
import com.chwl.app.ui.utils.ImageLoadUtils;
import com.chwl.app.ui.widget.TextSpannableBuilder;
import com.chwl.app.utils.AvatarHelper;
import com.chwl.core.decoration.headwear.bean.HeadWearInfo;
import com.chwl.core.level.UserLevelResourceType;
import com.chwl.core.noble.NobleUtil;
import com.chwl.core.user.bean.UserInfo;
import com.chwl.library.widget.SVGAView;
import com.example.lib_utils.StringUtils2;
import com.example.lib_utils.UiUtils;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.api.model.team.AvatarClickListener;
import com.netease.nim.uikit.common.ui.imageview.HeadImageView;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
import com.netease.nim.uikit.common.ui.recyclerview.holder.NIMBaseViewHolder;
import com.netease.nim.uikit.common.ui.recyclerview.holder.PublicChatRoomNimBaseViewHolder;
import com.netease.nim.uikit.common.ui.recyclerview.holder.RecyclerViewHolder;
import com.netease.nim.uikit.common.util.sys.TimeUtil;
import com.netease.nim.uikit.impl.NimUIKitImpl;
import com.netease.nimlib.sdk.NIMClient;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
import com.netease.nimlib.sdk.msg.MsgService;
import com.netease.nimlib.sdk.msg.attachment.FileAttachment;
import com.netease.nimlib.sdk.msg.constant.MsgDirectionEnum;
import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
/**
* 会话窗口消息列表项的ViewHolder基类负责每个消息项的外层框架包括头像昵称发送/接收进度条,重发按钮等。<br>
* 具体的消息展示项可继承该基类,然后完成具体消息内容展示即可。
*/
@Keep
public abstract class ChatRoomMessageViewHolderBase extends RecyclerViewHolder<BaseMultiItemFetchLoadAdapter, PublicChatRoomNimBaseViewHolder, ChatRoomMessage> {
public ImageView nameIconView;
// basic
protected View view;
protected Context context;
protected BaseMultiItemFetchLoadAdapter adapter;
// data
protected ChatRoomMessage message;
// view
protected View alertButton;
protected TextView timeTextView;
protected ProgressBar progressBar;
protected TextView nameTextView;
protected FrameLayout contentContainer;
protected LinearLayout nameContainer;
protected TextView readReceiptTextView;
// contentContainerView的默认长按事件。如果子类需要不同的处理可覆盖onItemLongClick方法
// 但如果某些子控件会拦截触摸消息导致contentContainer收不到长按事件子控件也可在inflate时重新设置
protected View.OnLongClickListener longClickListener;
private HeadImageView avatarLeft;
private HeadImageView avatarRight;
private SVGAView headWearLeft;
private SVGAView headWearRight;
private View headWearLeftLayout;
private View headWearRightLayout;
private int expLevelHeight = UiUtils.INSTANCE.dip2px(18f);
public ChatRoomMessageViewHolderBase(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
this.adapter = adapter;
}
/// -- 以下接口可由子类覆盖或实现
// 返回具体消息类型内容展示区域的layout res id
abstract protected int getContentResId();
// 在该接口中根据layout对各控件成员变量赋值
abstract protected void inflateContentView();
// 在该接口操作BaseViewHolder中的数据进行事件绑定可选
protected void bindHolder(NIMBaseViewHolder holder) {
}
// 将消息数据项与内容的view进行绑定
abstract protected void bindContentView();
// 内容区域点击事件响应处理。
protected void onItemClick() {
}
// 内容区域长按事件响应处理。该接口的优先级比adapter中有长按事件的处理监听高当该接口返回为true时adapter的长按事件监听不会被调用到。
protected boolean onItemLongClick() {
return false;
}
// 当是接收到的消息时内容区域背景的drawable id
protected int leftBackground() {
return R.drawable.bg_nim_water_drop_other_chatroom;
}
// 当是发送出去的消息时内容区域背景的drawable id
protected int rightBackground() {
return R.drawable.bg_nim_water_drop_self_chatroom;
}
// 返回该消息是不是居中显示
protected boolean isMiddleItem() {
return false;
}
// 是否显示头像,默认为显示
protected boolean isShowHeadImage() {
return true;
}
// 是否显示气泡背景,默认为显示
protected boolean isShowBubble() {
return true;
}
// 是否显示已读,默认为显示
protected boolean shouldDisplayReceipt() {
return true;
}
/// -- 以下接口可由子类调用
protected final ChatRoomMessageAdapter getMsgAdapter() {
return (ChatRoomMessageAdapter) adapter;
}
protected boolean shouldDisplayNick() {
return isReceivedMessage() && !isMiddleItem();
}
/**
* 下载附件/缩略图
*/
protected void downloadAttachment() {
if (message.getAttachment() != null && message.getAttachment() instanceof FileAttachment)
NIMClient.getService(MsgService.class).downloadAttachment(message, true);
}
// 设置FrameLayout子控件的gravity参数
protected final void setGravity(View view, int gravity) {
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams();
params.gravity = gravity;
}
// 设置控件的长宽
protected void setLayoutParams(int width, int height, View... views) {
for (View view : views) {
ViewGroup.LayoutParams maskParams = view.getLayoutParams();
maskParams.width = width;
maskParams.height = height;
view.setLayoutParams(maskParams);
}
}
// 根据layout id查找对应的控件
protected <T extends View> T findViewById(int id) {
return (T) view.findViewById(id);
}
// 判断消息方向,是否是接收到的消息
protected boolean isReceivedMessage() {
return message.getDirect() == MsgDirectionEnum.In;
}
/// -- 以下是基类实现代码
@Override
public void convert(PublicChatRoomNimBaseViewHolder holder, ChatRoomMessage data, int position, boolean isScrolling) {
view = holder.getConvertView();
context = holder.getContext();
message = data;
inflate(holder);
refresh();
bindHolder(holder);
}
protected final void inflate(PublicChatRoomNimBaseViewHolder holder) {
timeTextView = findViewById(R.id.message_item_time);
avatarLeft = findViewById(R.id.message_item_portrait_left);
avatarRight = findViewById(R.id.message_item_portrait_right);
headWearLeftLayout = findViewById(R.id.message_item_portrait_left_layout);
headWearRightLayout = findViewById(R.id.message_item_portrait_right_layout);
headWearLeft = findViewById(R.id.message_item_head_wear_left);
headWearRight = findViewById(R.id.message_item_head_wear_right);
alertButton = findViewById(R.id.message_item_alert);
progressBar = findViewById(R.id.message_item_progress);
nameTextView = findViewById(R.id.message_item_nickname);
contentContainer = findViewById(R.id.message_item_content);
nameIconView = findViewById(R.id.message_item_name_icon);
nameContainer = findViewById(R.id.message_item_name_layout);
readReceiptTextView = findViewById(R.id.textViewAlreadyRead);
// 这里只要inflate出来后加入一次即可
if (contentContainer.getChildCount() == 0) {
View.inflate(view.getContext(), getContentResId(), contentContainer);
}
inflateContentView();
headWearLeft.bindCache(holder.getSvgaCache());
headWearRight.bindCache(holder.getSvgaCache());
}
protected final void refresh() {
setHeadImageView();
setNameTextView();
setTimeTextView();
setStatus();
setOnClickListener();
setLongClickListener();
setContent();
setReadReceipt();
bindContentView();
loadBubble();
}
public void refreshCurrentItem() {
if (message != null) {
refresh();
}
}
/**
* 设置时间显示
*/
private void setTimeTextView() {
if (getMsgAdapter().needShowTime(message)) {
timeTextView.setVisibility(View.VISIBLE);
} else {
timeTextView.setVisibility(View.GONE);
return;
}
String text = TimeUtil.getTimeShowString(message.getTime(), false);
timeTextView.setText(text);
}
/**
* 设置消息发送状态
*/
private void setStatus() {
MsgStatusEnum status = message.getStatus();
switch (status) {
case fail:
progressBar.setVisibility(View.GONE);
alertButton.setVisibility(View.VISIBLE);
break;
case sending:
progressBar.setVisibility(View.VISIBLE);
alertButton.setVisibility(View.GONE);
break;
default:
progressBar.setVisibility(View.GONE);
alertButton.setVisibility(View.GONE);
break;
}
}
private void setHeadImageView() {
View showLayout = isReceivedMessage() ? headWearLeftLayout : headWearRightLayout;
View hideLayout = isReceivedMessage() ? headWearRightLayout : headWearLeftLayout;
HeadImageView showAvatarView = isReceivedMessage() ? avatarLeft : avatarRight;
SVGAView showHeadWearView = isReceivedMessage() ? headWearLeft : headWearRight;
SVGAView hideHeadWearView = isReceivedMessage() ? headWearRight : headWearLeft;
hideLayout.setVisibility(View.GONE);
clearHeadWear(hideHeadWearView);
if (!isShowHeadImage()) {
clearHeadWear(showHeadWearView);
showLayout.setVisibility(View.GONE);
return;
}
if (isMiddleItem()) {
clearHeadWear(showHeadWearView);
showLayout.setVisibility(View.GONE);
} else {
showLayout.setVisibility(View.VISIBLE);
String avatar = NobleUtil.getResource(UserInfo.AVATAR, message);
if (avatar.length() != 0) {
showAvatarView.loadAvatar(avatar);
} else {
showAvatarView.loadBuddyAvatar(message);
}
String headWear = NobleUtil.getResource(HeadWearInfo.HEAD_WEAR, message);
int headWearType = StringUtils2.INSTANCE.toInt(NobleUtil.getResource(HeadWearInfo.TYPE, message));
if (headWear.length() != 0) {
if (!headWear.equals(showHeadWearView.getTag(com.chwl.app.R.id.mic_item_head_wear))) {
showHeadWearView.setVisibility(View.VISIBLE);
showHeadWearView.setTag(com.chwl.app.R.id.mic_item_head_wear, headWear);
AvatarHelper.loadAvatarFrame(showHeadWearView, headWear, headWearType);
}
} else {
clearHeadWear(showHeadWearView);
}
}
}
private void clearHeadWear(SVGAView view) {
if (view == null) return;
view.clearAnimation();
view.stopAnimation();
view.setImageDrawable(null);
view.setTag(com.chwl.app.R.id.mic_item_head_wear, null);
view.setVisibility(View.GONE);
}
private void setOnClickListener() {
// 重发/重收按钮响应事件
if (getMsgAdapter().getEventListener() != null) {
alertButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getMsgAdapter().getEventListener().onFailedBtnClick(message);
}
});
}
// 内容区域点击事件响应, 相当于点击了整项
contentContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClick();
}
});
// 头像点击事件响应
if (NimUIKitImpl.getSessionListener() != null) {
View.OnClickListener portraitListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
String sessionId = message.getSessionId();
if (!TextUtils.isEmpty(sessionId)) {
AvatarClickListener avatarClickListener = NimUIKitImpl.getAvatarClickListener(sessionId);
if (avatarClickListener != null) {
avatarClickListener.avatarClick(message.getFromAccount());
} else if (NimUIKitImpl.getSessionListener() != null) {
NimUIKitImpl.getSessionListener().onAvatarClicked(context, message);
}
} else {
if (NimUIKitImpl.getSessionListener() != null) {
NimUIKitImpl.getSessionListener().onAvatarClicked(context, message);
}
}
}
};
avatarLeft.setOnClickListener(portraitListener);
avatarRight.setOnClickListener(portraitListener);
}
}
/**
* item长按事件监听
*/
private void setLongClickListener() {
longClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
// 优先派发给自己处理,
if (!onItemLongClick()) {
if (getMsgAdapter().getEventListener() != null) {
getMsgAdapter().getEventListener().onViewHolderLongClick(contentContainer, view, message);
return true;
}
}
return false;
}
};
// 消息长按事件响应处理
contentContainer.setOnLongClickListener(longClickListener);
// 头像长按事件响应处理
if (NimUIKitImpl.getSessionListener() != null) {
View.OnLongClickListener longClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (NimUIKitImpl.getSessionListener() != null) {
NimUIKitImpl.getSessionListener().onAvatarLongClicked(context, message);
}
return true;
}
};
avatarLeft.setOnLongClickListener(longClickListener);
avatarRight.setOnLongClickListener(longClickListener);
}
}
private void setNameTextView() {
if (!shouldDisplayNick()) {
nameTextView.setVisibility(View.GONE);
return;
}
nameTextView.setVisibility(View.VISIBLE);
TextSpannableBuilder builder = new TextSpannableBuilder(nameTextView);
String nickName = getNameText();
builder.append(nickName);
addCommonTag(message, builder, nameTextView);
nameTextView.setText(builder.build());
}
private void addCommonTag(ChatRoomMessage chatRoomMessage, @NonNull TextSpannableBuilder builder, TextView tvContent) {
String userLevel = NobleUtil.getLevel(UserLevelResourceType.EXPER_URL, chatRoomMessage);
String charmLevel = NobleUtil.getLevel(UserLevelResourceType.CHARM_URL, chatRoomMessage);
//等級
builder.append(userLevel, expLevelHeight);
builder.append(charmLevel, expLevelHeight);
}
protected String getNameText() {
String name = NobleUtil.getNamePlate(UserInfo.NICK, message);
if (name.length() > 6) {
return name.substring(0, 6) + "..";
}
return name;
}
private void setContent() {
if (!isShowBubble() && !isMiddleItem()) {
return;
}
LinearLayout bodyContainer = view.findViewById(R.id.message_item_body);
// 调整container的位置
int index = isReceivedMessage() ? 0 : 3;
if (bodyContainer.getChildAt(index) != contentContainer) {
bodyContainer.removeView(contentContainer);
bodyContainer.addView(contentContainer, index);
}
if (isMiddleItem()) {
setGravity(bodyContainer, Gravity.CENTER);
} else {
if (isReceivedMessage()) {
setGravity(bodyContainer, Gravity.START);
} else {
setGravity(bodyContainer, Gravity.END);
}
}
}
private void loadBubble(){
if (!isShowBubble() || isMiddleItem()) {
return;
}
onLoadBubble();
}
protected void onLoadBubble() {
if (isReceivedMessage()) {
contentContainer.setBackgroundResource(leftBackground());
} else {
contentContainer.setBackgroundResource(rightBackground());
}
}
protected void loadCustomBubble(Consumer<Boolean> callback) {
String androidBubbleUrl = NobleUtil.getResource(UserInfo.BUBBLE_URL_ANDROID, message);
if (TextUtils.isEmpty(androidBubbleUrl)) return;
int padding = UiUtils.INSTANCE.dip2px(12);
contentContainer.setPadding(padding, padding, padding, padding);
ImageLoadUtils.loadNinePatchBg(contentContainer, androidBubbleUrl, callback);
}
private void setReadReceipt() {
if (shouldDisplayReceipt() && !TextUtils.isEmpty(getMsgAdapter().getUuid()) && message.getUuid().equals(getMsgAdapter().getUuid())) {
readReceiptTextView.setVisibility(View.VISIBLE);
} else {
readReceiptTextView.setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,89 @@
package com.chwl.app.public_chat.core.viewholder;
import androidx.annotation.Keep;
import com.chwl.core.im.custom.bean.HeadlineChangedAttachment;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomNotificationAttachment;
import com.netease.nimlib.sdk.msg.attachment.AudioAttachment;
import com.netease.nimlib.sdk.msg.attachment.ImageAttachment;
import com.netease.nimlib.sdk.msg.attachment.MsgAttachment;
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* 消息项展示ViewHolder工厂类。
*/
@Keep
public class ChatRoomMessageViewHolderFactory {
private static HashMap<Class<? extends MsgAttachment>, Class<? extends ChatRoomMessageViewHolderBase>> viewHolders = new HashMap<>();
private static Class<? extends ChatRoomMessageViewHolderBase> tipMsgViewHolder;
static {
// built in
register(ImageAttachment.class, ChatRoomMessageViewHolderPicture.class);
register(AudioAttachment.class, ChatRoomMessageViewHolderAudio.class);
register(ChatRoomNotificationAttachment.class, ChatRoomMessageViewHolderNotification.class);
register(HeadlineChangedAttachment.class, ChatRoomMessageViewHolderHeadline.class);
}
public static void register(Class<? extends MsgAttachment> attach, Class<? extends ChatRoomMessageViewHolderBase> viewHolder) {
viewHolders.put(attach, viewHolder);
}
public static void registerTipMsgViewHolder(Class<? extends ChatRoomMessageViewHolderBase> viewHolder) {
tipMsgViewHolder = viewHolder;
}
public static Class<? extends ChatRoomMessageViewHolderBase> getViewHolderByType(ChatRoomMessage message) {
if (message.getMsgType() == MsgTypeEnum.text) {
return ChatRoomMessageViewHolderText.class;
} else if (message.getMsgType() == MsgTypeEnum.tip) {
return tipMsgViewHolder == null ? ChatRoomMessageViewHolderUnknown.class : tipMsgViewHolder;
} else {
Class<? extends ChatRoomMessageViewHolderBase> viewHolder = null;
if (message.getAttachment() != null) {
Class<? extends MsgAttachment> clazz = message.getAttachment().getClass();
while (viewHolder == null && clazz != null) {
viewHolder = viewHolders.get(clazz);
if (viewHolder == null) {
clazz = getSuperClass(clazz);
}
}
}
return viewHolder == null ? ChatRoomMessageViewHolderUnknown.class : viewHolder;
}
}
private static Class<? extends MsgAttachment> getSuperClass(Class<? extends MsgAttachment> derived) {
Class sup = derived.getSuperclass();
if (sup != null && MsgAttachment.class.isAssignableFrom(sup)) {
return sup;
} else {
for (Class itf : derived.getInterfaces()) {
if (MsgAttachment.class.isAssignableFrom(itf)) {
return itf;
}
}
}
return null;
}
public static List<Class<? extends ChatRoomMessageViewHolderBase>> getAllViewHolders() {
List<Class<? extends ChatRoomMessageViewHolderBase>> list = new ArrayList<>();
list.addAll(viewHolders.values());
if (tipMsgViewHolder != null) {
list.add(tipMsgViewHolder);
}
list.add(ChatRoomMessageViewHolderUnknown.class);
list.add(ChatRoomMessageViewHolderText.class);
return list;
}
}

View File

@@ -0,0 +1,47 @@
package com.chwl.app.public_chat.core.viewholder
import android.view.View
import android.widget.TextView
import androidx.annotation.Keep
import com.chwl.app.R
import com.chwl.core.im.custom.bean.HeadlineChangedAttachment
import com.example.lib_utils.ktx.getColorById
import com.example.lib_utils.spannable.SpannableTextBuilder
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter
@Keep
class ChatRoomMessageViewHolderHeadline(adapter: BaseMultiItemFetchLoadAdapter<*, *>) :
ChatRoomMessageViewHolderBase(adapter) {
private var textView: TextView? = null
override fun bindContentView() {
val data = (message.attachment as? HeadlineChangedAttachment)?.headlineData
val nick = data?.nick ?: ""
textView?.let {
SpannableTextBuilder(it).appendText(
context.getString(
R.string.headline_message_format,
nick
)
).setTextStyle(nick, textColor = context.getColorById(R.color.color_DE3446)).apply()
}
}
override fun getContentResId(): Int {
return R.layout.public_chat_message_item_headline
}
override fun shouldDisplayNick(): Boolean {
return false
}
override fun inflateContentView() {
textView =
view.findViewById<View>(R.id.tv_content) as TextView
}
override fun isMiddleItem(): Boolean {
return true
}
}

View File

@@ -0,0 +1,46 @@
package com.chwl.app.public_chat.core.viewholder;
import android.widget.TextView;
import androidx.annotation.Keep;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.business.chatroom.helper.ChatRoomNotificationHelper;
import com.netease.nim.uikit.business.chatroom.viewholder.ChatRoomMsgViewHolderBase;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomNotificationAttachment;
@Keep
public class ChatRoomMessageViewHolderNotification extends ChatRoomMessageViewHolderBase {
protected TextView notificationTextView;
public ChatRoomMessageViewHolderNotification(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
}
@Override
protected int getContentResId() {
return R.layout.nim_message_item_notification;
}
@Override
protected boolean shouldDisplayNick() {
return false;
}
@Override
protected void inflateContentView() {
notificationTextView = (TextView) view.findViewById(R.id.message_item_notification_label);
}
@Override
protected void bindContentView() {
notificationTextView.setText(ChatRoomNotificationHelper.getNotificationText((ChatRoomNotificationAttachment) message.getAttachment()));
}
@Override
protected boolean isMiddleItem() {
return true;
}
}

View File

@@ -0,0 +1,39 @@
package com.chwl.app.public_chat.core.viewholder;
import androidx.annotation.Keep;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.business.session.activity.WatchMessagePictureActivity;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
/**
* Created by zhoujianghua on 2015/8/4.
*/
@Keep
public class ChatRoomMessageViewHolderPicture extends ChatRoomMessageViewHolderThumbBase {
public ChatRoomMessageViewHolderPicture(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
}
@Override
protected int getContentResId() {
return R.layout.nim_message_item_picture;
}
@Override
protected void onItemClick() {
WatchMessagePictureActivity.start(context, message);
}
@Override
protected String thumbFromSourceFile(String path) {
return path;
}
@Override
protected void onLoadBubble() {
super.onLoadBubble();
loadCustomBubble(null);
}
}

View File

@@ -0,0 +1,67 @@
package com.chwl.app.public_chat.core.viewholder;
import android.text.method.LinkMovementMethod;
import android.text.style.ImageSpan;
import android.widget.TextView;
import androidx.annotation.Keep;
import androidx.core.content.ContextCompat;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.api.NimUIKit;
import com.netease.nim.uikit.business.session.emoji.MoonUtil;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
/**
* Created by zhoujianghua on 2015/8/4.
*/
@Keep
public class ChatRoomMessageViewHolderText extends ChatRoomMessageViewHolderBase {
protected TextView bodyTextView;
public ChatRoomMessageViewHolderText(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
}
@Override
protected int getContentResId() {
return R.layout.nim_message_item_text;
}
@Override
protected void inflateContentView() {
bodyTextView = findViewById(R.id.nim_message_item_text_body);
}
@Override
protected void bindContentView() {
loadTextColor();
bodyTextView.setOnClickListener(v -> onItemClick());
MoonUtil.identifyFaceExpression(NimUIKit.getContext(), bodyTextView, getDisplayText(), ImageSpan.ALIGN_BOTTOM);
bodyTextView.setMovementMethod(LinkMovementMethod.getInstance());
bodyTextView.setOnLongClickListener(longClickListener);
}
@Override
protected void onLoadBubble() {
super.onLoadBubble();
loadCustomBubble(aBoolean -> {
if (aBoolean) {
bodyTextView.setTextColor(ContextCompat.getColor(context, R.color.color_333333));
}
});
}
private void loadTextColor() {
int textColor = ContextCompat.getColor(context, R.color.color_333333);
if (isReceivedMessage()) {
textColor = ContextCompat.getColor(context, R.color.color_white);
}
bodyTextView.setTextColor(textColor);
bodyTextView.setLinkTextColor(textColor);
}
protected String getDisplayText() {
return message.getContent();
}
}

View File

@@ -0,0 +1,135 @@
package com.chwl.app.public_chat.core.viewholder;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.Keep;
import com.chwl.app.public_chat.core.viewholder.ChatRoomMessageViewHolderBase;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.common.ui.imageview.MsgThumbImageView;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
import com.netease.nim.uikit.common.util.media.BitmapDecoder;
import com.netease.nim.uikit.common.util.media.ImageUtil;
import com.netease.nim.uikit.common.util.string.StringUtil;
import com.netease.nim.uikit.common.util.sys.ScreenUtil;
import com.netease.nimlib.sdk.msg.attachment.FileAttachment;
import com.netease.nimlib.sdk.msg.attachment.ImageAttachment;
import com.netease.nimlib.sdk.msg.attachment.VideoAttachment;
import com.netease.nimlib.sdk.msg.constant.AttachStatusEnum;
import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
import java.io.File;
/**
* Created by zhoujianghua on 2015/8/4.
*/
@Keep
public abstract class ChatRoomMessageViewHolderThumbBase extends ChatRoomMessageViewHolderBase {
public ChatRoomMessageViewHolderThumbBase(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
}
protected MsgThumbImageView thumbnail;
protected View progressCover;
protected TextView progressLabel;
@Override
protected void inflateContentView() {
thumbnail = findViewById(R.id.message_item_thumb_thumbnail);
progressBar = findViewById(R.id.message_item_thumb_progress_bar); // 覆盖掉
progressCover = findViewById(R.id.message_item_thumb_progress_cover);
progressLabel = findViewById(R.id.message_item_thumb_progress_text);
}
@Override
protected void bindContentView() {
FileAttachment msgAttachment = (FileAttachment) message.getAttachment();
String path = msgAttachment.getPath();
String thumbPath = msgAttachment.getThumbPath();
if (!TextUtils.isEmpty(thumbPath)) {
loadThumbnailImage(thumbPath, false, msgAttachment.getExtension());
} else if (!TextUtils.isEmpty(path)) {
loadThumbnailImage(thumbFromSourceFile(path), true, msgAttachment.getExtension());
} else {
loadThumbnailImage(null, false, msgAttachment.getExtension());
if (message.getAttachStatus() == AttachStatusEnum.transferred
|| message.getAttachStatus() == AttachStatusEnum.def) {
downloadAttachment();
}
}
refreshStatus();
}
private void refreshStatus() {
FileAttachment attachment = (FileAttachment) message.getAttachment();
if (TextUtils.isEmpty(attachment.getPath()) && TextUtils.isEmpty(attachment.getThumbPath())) {
if (message.getAttachStatus() == AttachStatusEnum.fail || message.getStatus() == MsgStatusEnum.fail) {
alertButton.setVisibility(View.VISIBLE);
} else {
alertButton.setVisibility(View.GONE);
}
}
if (message.getStatus() == MsgStatusEnum.sending
|| (isReceivedMessage() && message.getAttachStatus() == AttachStatusEnum.transferring)) {
progressCover.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
progressLabel.setVisibility(View.VISIBLE);
progressLabel.setText(StringUtil.getPercentString(getMsgAdapter().getProgress(message)));
} else {
progressCover.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
progressLabel.setVisibility(View.GONE);
}
}
private void loadThumbnailImage(String path, boolean isOriginal, String ext) {
setImageSize(path);
if (path != null) {
//thumbnail.loadAsPath(thumbPath, getImageMaxEdge(), getImageMaxEdge(), maskBg());
thumbnail.loadAsPath(path, getImageMaxEdge(), getImageMaxEdge(), maskBg(), ext);
} else {
thumbnail.loadAsResource(R.drawable.nim_image_default, maskBg());
}
}
private void setImageSize(String thumbPath) {
int[] bounds = null;
if (thumbPath != null) {
bounds = BitmapDecoder.decodeBound(new File(thumbPath));
}
if (bounds == null) {
if (message.getMsgType() == MsgTypeEnum.image) {
ImageAttachment attachment = (ImageAttachment) message.getAttachment();
bounds = new int[]{attachment.getWidth(), attachment.getHeight()};
} else if (message.getMsgType() == MsgTypeEnum.video) {
VideoAttachment attachment = (VideoAttachment) message.getAttachment();
bounds = new int[]{attachment.getWidth(), attachment.getHeight()};
}
}
if (bounds != null) {
ImageUtil.ImageSize imageSize = ImageUtil.getThumbnailDisplaySize(bounds[0], bounds[1], getImageMaxEdge(), getImageMinEdge());
setLayoutParams(imageSize.width, imageSize.height, thumbnail);
}
}
private int maskBg() {
return R.drawable.nim_message_item_round_bg;
}
public static int getImageMaxEdge() {
return (int) (165.0 / 320.0 * ScreenUtil.screenWidth);
}
public static int getImageMinEdge() {
return (int) (76.0 / 320.0 * ScreenUtil.screenWidth);
}
protected abstract String thumbFromSourceFile(String path);
}

View File

@@ -0,0 +1,39 @@
package com.chwl.app.public_chat.core.viewholder;
import androidx.annotation.Keep;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseMultiItemFetchLoadAdapter;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
/**
* Created by zhoujianghua on 2015/8/6.
*/
@Keep
public class ChatRoomMessageViewHolderUnknown extends ChatRoomMessageViewHolderBase {
public ChatRoomMessageViewHolderUnknown(BaseMultiItemFetchLoadAdapter adapter) {
super(adapter);
}
@Override
protected int getContentResId() {
return R.layout.nim_message_item_unknown;
}
@Override
protected boolean isShowHeadImage() {
if (message.getSessionType() == SessionTypeEnum.ChatRoom) {
return false;
}
return true;
}
@Override
protected void inflateContentView() {
}
@Override
protected void bindContentView() {
}
}

View File

@@ -0,0 +1,37 @@
package com.chwl.app.public_chat.ui.message
import androidx.lifecycle.MutableLiveData
import com.chwl.app.base.BaseViewModel
import com.chwl.core.bean.response.BeanResult
import com.chwl.core.public_chat_hall.model.PublicChatModel
import kotlinx.coroutines.flow.MutableSharedFlow
class HeadlineViewModel : BaseViewModel() {
val sendHeadlineFlow = MutableSharedFlow<BeanResult<Any>>()
val headlinePayMoneyLiveData = MutableLiveData<Long?>()
fun sendHeadline(message: String) {
safeLaunch(needLoading = true, onError = {
sendHeadlineFlow.emit(BeanResult.failed(it))
}) {
// PublicChatModel.sendHeadline(message)
// sendHeadlineFlow.emit(BeanResult.success(true))
}
}
fun getHeadlinePayMoneyIsNull() {
if (headlinePayMoneyLiveData.value == null) {
getHeadlinePayMoney()
}
}
fun getHeadlinePayMoney() {
safeLaunch(needLoading = false, onError = {
}) {
// val value = PublicChatModel.getHeadlinePayMoney()
// headlinePayMoneyLiveData.postValue(value)
}
}
}

View File

@@ -0,0 +1,977 @@
package com.chwl.app.public_chat.ui.message;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import android.os.SystemClock;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.Chronometer;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.alibaba.fastjson2.JSONObject;
import com.chwl.library.utils.ResUtil;
import com.chwl.library.utils.SingleToastUtil;
import com.chwl.library.utils.SizeUtils;
import com.netease.nim.uikit.R;
import com.netease.nim.uikit.api.NimUIKit;
import com.netease.nim.uikit.api.UIKitOptions;
import com.netease.nim.uikit.api.model.session.SessionCustomization;
import com.netease.nim.uikit.business.ait.AitTextChangeListener;
import com.netease.nim.uikit.business.session.actions.BaseAction;
import com.netease.nim.uikit.business.session.emoji.EmoticonPickerView;
import com.netease.nim.uikit.business.session.emoji.IEmoticonSelectedListener;
import com.netease.nim.uikit.business.session.emoji.MoonUtil;
import com.netease.nim.uikit.business.session.event.ActiveEvent;
import com.netease.nim.uikit.business.session.module.Container;
import com.netease.nim.uikit.business.session.module.input.ActionsPanel;
import com.netease.nim.uikit.common.antispam.AntiSpamEvent;
import com.netease.nim.uikit.common.ui.dialog.EasyAlertDialogHelper;
import com.netease.nim.uikit.common.util.AntiSpamUtil;
import com.netease.nim.uikit.common.util.log.LogUtil;
import com.netease.nim.uikit.common.util.string.StringUtil;
import com.netease.nim.uikit.impl.NimUIKitImpl;
import com.netease.nimlib.sdk.NIMClient;
import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder;
import com.netease.nimlib.sdk.media.record.AudioRecorder;
import com.netease.nimlib.sdk.media.record.IAudioRecordCallback;
import com.netease.nimlib.sdk.media.record.RecordType;
import com.netease.nimlib.sdk.msg.MsgService;
import com.netease.nimlib.sdk.msg.attachment.MsgAttachment;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.msg.model.CustomNotification;
import com.netease.nimlib.sdk.msg.model.CustomNotificationConfig;
import com.netease.nimlib.sdk.msg.model.IMMessage;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.util.List;
/**
* 底部文本编辑,语音等模块
* Created by hzxuwen on 2015/6/16.
*/
public class PublicChatRoomInputPanel implements IEmoticonSelectedListener, IAudioRecordCallback, AitTextChangeListener {
private static final String TAG = "MsgSendLayout";
private static final int SHOW_LAYOUT_DELAY = 200;
protected Container container;
protected View view;
protected Handler uiHandler;
protected View actionPanelBottomLayout; // 更多布局
protected LinearLayout messageActivityBottomLayout;
protected EditText messageEditText;// 文本消息编辑框
protected Button audioRecordBtn; // 录音按钮
protected View audioAnimLayout; // 录音动画布局
protected FrameLayout textAudioSwitchLayout; // 切换文本,语音按钮布局
protected View switchToTextButtonInInputBar;// 文本消息选择按钮
protected View switchToAudioButtonInInputBar;// 语音消息选择按钮
protected View moreFuntionButtonInInputBar;// 更多消息选择按钮
protected View sendMessageButtonInInputBar;// 发送消息按钮
protected View emojiButtonInInputBar;// 发送消息按钮
protected View messageInputBar;
private SessionCustomization customization;
// 表情
protected EmoticonPickerView emoticonPickerView; // 贴图表情控件
// 语音
protected AudioRecorder audioMessageHelper;
private Chronometer time;
private TextView timerTip;
private LinearLayout timerTipContainer;
private boolean started = false;
private boolean cancelled = false;
private boolean touched = false; // 是否按着
private boolean isKeyboardShowed = true; // 是否显示键盘
// state
private boolean actionPanelBottomLayoutHasSetup = false;
private boolean isTextAudioSwitchShow = true; // 是否展示左侧语音按钮
// adapter
private List<BaseAction> actions;
// data
private long typingTime = 0;
private boolean isRobotSession;
private TextWatcher aitTextWatcher;
private volatile boolean disable;
private boolean isChat;
public PublicChatRoomInputPanel(Container container, View view, List<BaseAction> actions, boolean isTextAudioSwitchShow) {
this.container = container;
this.view = view;
this.actions = actions;
this.uiHandler = new Handler();
this.isTextAudioSwitchShow = isTextAudioSwitchShow;
init();
}
public PublicChatRoomInputPanel(Container container, View view, List<BaseAction> actions) {
this(container, view, actions, true);
}
public void onPause() {
// 停止录音
if (audioMessageHelper != null) {
onEndAudioRecord(true);
}
}
public void onDestroy() {
// release
if (audioMessageHelper != null) {
audioMessageHelper.destroyAudioRecorder();
}
}
public boolean collapse(boolean immediately) {
boolean respond = (emoticonPickerView != null && emoticonPickerView.getVisibility() == View.VISIBLE
|| actionPanelBottomLayout != null && actionPanelBottomLayout.getVisibility() == View.VISIBLE);
hideAllInputLayout(immediately);
return respond;
}
public void addAitTextWatcher(TextWatcher watcher) {
aitTextWatcher = watcher;
}
public void removeAitTextWatcher(TextWatcher watcher) {
aitTextWatcher = watcher;
}
private void init() {
initViews();
initInputBarListener();
initTextEdit();
initAudioRecordButton();
restoreText(false);
for (int i = 0; i < actions.size(); ++i) {
actions.get(i).setIndex(i);
actions.get(i).setContainer(container);
}
if (disable) {
disableButtons();
}
}
public void setCustomization(SessionCustomization customization) {
this.customization = customization;
if (customization != null) {
emoticonPickerView.setWithSticker(customization.withSticker);
}
}
public void reload(Container container, SessionCustomization customization) {
this.container = container;
setCustomization(customization);
}
private void initViews() {
// input bar
messageActivityBottomLayout = view.findViewById(R.id.messageActivityBottomLayout);
messageInputBar = view.findViewById(R.id.textMessageLayout);
switchToTextButtonInInputBar = view.findViewById(R.id.buttonTextMessage);
switchToAudioButtonInInputBar = view.findViewById(R.id.buttonAudioMessage);
moreFuntionButtonInInputBar = view.findViewById(R.id.buttonMoreFuntionInText);
emojiButtonInInputBar = view.findViewById(R.id.emoji_button);
sendMessageButtonInInputBar = view.findViewById(R.id.buttonSendMessage);
messageEditText = view.findViewById(R.id.editTextMessage);
// 语音
audioRecordBtn = view.findViewById(R.id.audioRecord);
audioAnimLayout = view.findViewById(R.id.layoutPlayAudio);
time = view.findViewById(R.id.timer);
timerTip = view.findViewById(R.id.timer_tip);
timerTipContainer = view.findViewById(R.id.timer_tip_container);
// 表情
emoticonPickerView = view.findViewById(R.id.emoticon_picker_view);
// 显示录音按钮
switchToTextButtonInInputBar.setVisibility(View.GONE);
switchToAudioButtonInInputBar.setVisibility(View.VISIBLE);
// 文本录音按钮切换布局
textAudioSwitchLayout = view.findViewById(R.id.switchLayout);
if (isTextAudioSwitchShow) {
textAudioSwitchLayout.setVisibility(View.VISIBLE);
} else {
textAudioSwitchLayout.setVisibility(View.GONE);
}
}
public void disableButtons() {
if (messageEditText != null && audioRecordBtn != null &&
switchToTextButtonInInputBar != null && switchToAudioButtonInInputBar != null &&
moreFuntionButtonInInputBar != null && emojiButtonInInputBar != null) {
messageEditText.setEnabled(false);
messageEditText.setHint(ResUtil.getString(R.string.module_input_inputpanel_01));
messageEditText.setHintTextColor(Color.parseColor("#999999"));
audioRecordBtn.setEnabled(false);
switchToTextButtonInInputBar.setEnabled(false);
switchToAudioButtonInInputBar.setEnabled(false);
moreFuntionButtonInInputBar.setEnabled(false);
emojiButtonInInputBar.setEnabled(false);
} else {
setDisable(true);
}
}
public void setDisable(boolean disable) {
this.disable = disable;
}
private void initInputBarListener() {
switchToTextButtonInInputBar.setOnClickListener(clickListener);
switchToAudioButtonInInputBar.setOnClickListener(clickListener);
emojiButtonInInputBar.setOnClickListener(clickListener);
sendMessageButtonInInputBar.setOnClickListener(clickListener);
moreFuntionButtonInInputBar.setOnClickListener(clickListener);
}
private void initTextEdit() {
messageEditText.setInputType(InputType.TYPE_CLASS_TEXT);
messageEditText.setImeOptions(EditorInfo.IME_ACTION_SEND);
messageEditText.setOnEditorActionListener((v, actionId, event) -> {
//当actionId == XX_SEND 或者 XX_DONE时都触发
//或者event.getKeyCode == ENTER 且 event.getAction == ACTION_DOWN时也触发
//注意这是一定要判断event != null。因为在某些输入法上会返回null。
if (actionId == EditorInfo.IME_ACTION_SEND
|| actionId == EditorInfo.IME_ACTION_DONE
|| (event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode() && KeyEvent.ACTION_DOWN == event.getAction())) {
//处理事件
onTextMessageSendButtonPressed();
return true;
}
return false;
});
messageEditText.setOnFocusChangeListener((v, hasFocus) -> {
messageEditText.setHint("");
checkSendButtonEnable(messageEditText);
});
messageEditText.addTextChangedListener(new TextWatcher() {
private int start;
private int count;
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
this.start = start;
this.count = count;
if (aitTextWatcher != null) {
aitTextWatcher.onTextChanged(s, start, before, count);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (aitTextWatcher != null) {
aitTextWatcher.beforeTextChanged(s, start, count, after);
}
}
@Override
public void afterTextChanged(Editable s) {
checkSendButtonEnable(messageEditText);
MoonUtil.replaceEmoticons(container.activity, s, start, count);
int editEnd = messageEditText.getSelectionEnd();
messageEditText.removeTextChangedListener(this);
while (StringUtil.counterChars(s.toString()) > NimUIKitImpl.getOptions().maxInputTextLength && editEnd > 0) {
s.delete(editEnd - 1, editEnd);
editEnd--;
}
messageEditText.setSelection(Math.min(s.length(), editEnd));
messageEditText.addTextChangedListener(this);
if (aitTextWatcher != null) {
aitTextWatcher.afterTextChanged(s);
}
sendTypingCommand();
}
});
// 不展示右侧按钮时需要添加edit间距
if (!isTextAudioSwitchShow) {
try {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) messageEditText.getLayoutParams();
layoutParams.setMarginStart(SizeUtils.dp2px(view.getContext(), 12));
messageEditText.requestLayout();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
/**
* 发送“正在输入”通知
*/
private void sendTypingCommand() {
if (container.account.equals(NimUIKit.getAccount())) {
return;
}
if (container.sessionType == SessionTypeEnum.Team || container.sessionType == SessionTypeEnum.ChatRoom) {
return;
}
if (System.currentTimeMillis() - typingTime > 5000L) {
typingTime = System.currentTimeMillis();
CustomNotification command = new CustomNotification();
command.setSessionId(container.account);
command.setSessionType(container.sessionType);
CustomNotificationConfig config = new CustomNotificationConfig();
config.enablePush = false;
config.enableUnreadCount = false;
command.setConfig(config);
JSONObject json = new JSONObject();
json.put("id", "1");
command.setContent(json.toString());
NIMClient.getService(MsgService.class).sendCustomNotification(command);
}
}
/**
* ************************* 键盘布局切换 *******************************
*/
private final View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (v == switchToTextButtonInInputBar) {
switchToTextLayout(true);// 显示文本发送的布局
} else if (v == sendMessageButtonInInputBar) {
onTextMessageSendButtonPressed();
} else if (v == switchToAudioButtonInInputBar) {
switchToAudioLayout();
} else if (v == moreFuntionButtonInInputBar) {
toggleActionPanelLayout();
} else if (v == emojiButtonInInputBar) {
toggleEmojiLayout();
}
}
};
// 点击edittext切换键盘和更多布局
private void switchToTextLayout(boolean needShowInput) {
hideEmojiLayout();
hideActionPanelLayout();
audioRecordBtn.setVisibility(View.GONE);
messageEditText.setVisibility(View.VISIBLE);
switchToTextButtonInInputBar.setVisibility(View.GONE);
switchToAudioButtonInInputBar.setVisibility(View.VISIBLE);
messageInputBar.setVisibility(View.VISIBLE);
if (needShowInput) {
uiHandler.postDelayed(showTextRunnable, SHOW_LAYOUT_DELAY);
} else {
hideInputMethod();
}
}
// 发送文本消息
private void onTextMessageSendButtonPressed() {
String text = messageEditText.getText().toString();
if (TextUtils.isEmpty(text.trim())) return;
EventBus.getDefault().post(new ActiveEvent());
IMMessage textMessage = createTextMessage(text);
if (AntiSpamUtil.checkLocalAntiSpam(textMessage)) {
EventBus.getDefault().post(new AntiSpamEvent());
} else {
if (container.proxy.sendMessage(textMessage)) {
restoreText(true);
}
}
}
protected IMMessage createTextMessage(String text) {
return ChatRoomMessageBuilder.createChatRoomTextMessage(container.account, text);
}
// 切换成音频,收起键盘,按钮切换成键盘
private void switchToAudioLayout() {
if (!isChat) {
SingleToastUtil.showToast(ResUtil.getString(R.string.module_input_inputpanel_02));
return;
}
Log.e(TAG, "switchToAudioLayout: ");
container.proxy.onAudioClick(() -> {
messageEditText.setVisibility(View.GONE);
audioRecordBtn.setVisibility(View.VISIBLE);
hideInputMethod();
hideEmojiLayout();
hideActionPanelLayout();
switchToAudioButtonInInputBar.setVisibility(View.GONE);
switchToTextButtonInInputBar.setVisibility(View.VISIBLE);
});
}
// 点击“+”号按钮,切换更多布局和键盘
private void toggleActionPanelLayout() {
if (moreCustomId > -1) {
for (BaseAction action : actions) {
if (action.getTitleId() == moreCustomId) {
if (!isChat) {
SingleToastUtil.showToast(ResUtil.getString(R.string.module_input_inputpanel_03));
} else {
action.onClick();
}
break;
}
}
return;
}
if (actionPanelBottomLayout == null || actionPanelBottomLayout.getVisibility() == View.GONE) {
moreFuntionButtonInInputBar.setBackgroundResource(R.drawable.nim_message_input_plus_pressed);
showActionPanelLayout();
} else {
moreFuntionButtonInInputBar.setBackgroundResource(R.drawable.nim_message_input_plus);
hideActionPanelLayout();
}
}
// 点击表情,切换到表情布局
private void toggleEmojiLayout() {
if (!isChat) {
SingleToastUtil.showToast(ResUtil.getString(R.string.module_input_inputpanel_04));
} else {
if (emoticonPickerView == null || emoticonPickerView.getVisibility() == View.GONE) {
showEmojiLayout();
} else {
hideEmojiLayout();
}
}
}
// 隐藏表情布局
private void hideEmojiLayout() {
uiHandler.removeCallbacks(showEmojiRunnable);
if (emoticonPickerView != null) {
emoticonPickerView.setVisibility(View.GONE);
}
}
// 隐藏更多布局
private void hideActionPanelLayout() {
uiHandler.removeCallbacks(showMoreFuncRunnable);
if (actionPanelBottomLayout != null) {
actionPanelBottomLayout.setVisibility(View.GONE);
}
if (!isCustomOption)
moreFuntionButtonInInputBar.setBackgroundResource(R.drawable.nim_message_input_plus);
}
private boolean isCustomOption;
// 隐藏键盘布局
public void hideInputMethod() {
isKeyboardShowed = false;
uiHandler.removeCallbacks(showTextRunnable);
InputMethodManager imm = (InputMethodManager) container.activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(messageEditText.getWindowToken(), 0);
messageEditText.clearFocus();
}
// 隐藏语音布局
private void hideAudioLayout() {
audioRecordBtn.setVisibility(View.GONE);
messageEditText.setVisibility(View.VISIBLE);
switchToTextButtonInInputBar.setVisibility(View.VISIBLE);
switchToAudioButtonInInputBar.setVisibility(View.GONE);
}
// 显示表情布局
private void showEmojiLayout() {
hideInputMethod();
hideActionPanelLayout();
hideAudioLayout();
messageEditText.requestFocus();
uiHandler.postDelayed(showEmojiRunnable, 200);
emoticonPickerView.setVisibility(View.VISIBLE);
emoticonPickerView.show(this);
container.proxy.onInputPanelExpand();
}
// 初始化更多布局
private void addActionPanelLayout() {
if (actionPanelBottomLayout == null) {
View.inflate(container.activity, R.layout.nim_message_activity_actions_layout, messageActivityBottomLayout);
actionPanelBottomLayout = view.findViewById(R.id.actionsLayout);
actionPanelBottomLayoutHasSetup = false;
}
initActionPanelLayout();
}
// 显示键盘布局
private void showInputMethod(EditText editTextMessage) {
editTextMessage.requestFocus();
//如果已经显示,则继续操作时不需要把光标定位到最后
if (!isKeyboardShowed) {
editTextMessage.setSelection(editTextMessage.getText().length());
isKeyboardShowed = true;
}
InputMethodManager imm = (InputMethodManager) container.activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editTextMessage, 0);
container.proxy.onInputPanelExpand();
}
// 显示更多布局
private void showActionPanelLayout() {
addActionPanelLayout();
hideEmojiLayout();
hideInputMethod();
uiHandler.postDelayed(showMoreFuncRunnable, SHOW_LAYOUT_DELAY);
container.proxy.onInputPanelExpand();
}
// 初始化具体more layout中的项目
private void initActionPanelLayout() {
if (actionPanelBottomLayoutHasSetup) {
return;
}
ActionsPanel.init(view, actions);
actionPanelBottomLayoutHasSetup = true;
}
private final Runnable showEmojiRunnable = new Runnable() {
@Override
public void run() {
emoticonPickerView.setVisibility(View.VISIBLE);
}
};
private final Runnable showMoreFuncRunnable = new Runnable() {
@Override
public void run() {
actionPanelBottomLayout.setVisibility(View.VISIBLE);
}
};
private final Runnable showTextRunnable = new Runnable() {
@Override
public void run() {
showInputMethod(messageEditText);
}
};
private void restoreText(boolean clearText) {
if (clearText) {
messageEditText.setText("");
}
checkSendButtonEnable(messageEditText);
}
/**
* 显示发送或更多
*
* @param editText
*/
private void checkSendButtonEnable(EditText editText) {
if (isRobotSession) {
return;
}
String textMessage = editText.getText().toString();
setEditTextState();
if (!TextUtils.isEmpty(StringUtil.removeBlanks(textMessage)) && editText.hasFocus()) {
moreFuntionButtonInInputBar.setVisibility(View.GONE);
sendMessageButtonInInputBar.setVisibility(View.VISIBLE);
} else {
sendMessageButtonInInputBar.setVisibility(View.GONE);
moreFuntionButtonInInputBar.setVisibility(View.VISIBLE);
}
}
/**
* *************** IEmojiSelectedListener ***************
*/
@Override
public void onEmojiSelected(String key) {
Editable mEditable = messageEditText.getText();
if (key.equals("/DEL")) {
messageEditText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
} else {
int start = messageEditText.getSelectionStart();
int end = messageEditText.getSelectionEnd();
start = (start < 0 ? 0 : start);
end = (start < 0 ? 0 : end);
mEditable.replace(start, end, key);
}
}
private Runnable hideAllInputLayoutRunnable;
@Override
public void onStickerSelected(String category, String item) {
Log.i("InputPanel", "onStickerSelected, category =" + category + ", sticker =" + item);
if (customization != null) {
MsgAttachment attachment = customization.createStickerAttachment(category, item);
IMMessage stickerMessage = ChatRoomMessageBuilder.createChatRoomCustomMessage(container.account,attachment, ResUtil.getString(R.string.module_input_inputpanel_05));
container.proxy.sendMessage(stickerMessage);
}
}
@Override
public void onTextAdd(String content, int start, int length) {
if (messageEditText.getVisibility() != View.VISIBLE ||
(emoticonPickerView != null && emoticonPickerView.getVisibility() == View.VISIBLE)) {
switchToTextLayout(true);
} else {
uiHandler.postDelayed(showTextRunnable, SHOW_LAYOUT_DELAY);
}
messageEditText.getEditableText().insert(start, content);
}
@Override
public void onTextDelete(int start, int length) {
if (messageEditText.getVisibility() != View.VISIBLE) {
switchToTextLayout(true);
} else {
uiHandler.postDelayed(showTextRunnable, SHOW_LAYOUT_DELAY);
}
int end = start + length - 1;
messageEditText.getEditableText().replace(start, end, "");
}
public int getEditSelectionStart() {
return messageEditText.getSelectionStart();
}
/**
* 隐藏所有输入布局
*/
private void hideAllInputLayout(boolean immediately) {
if (hideAllInputLayoutRunnable == null) {
hideAllInputLayoutRunnable = new Runnable() {
@Override
public void run() {
hideInputMethod();
hideActionPanelLayout();
hideEmojiLayout();
}
};
}
long delay = immediately ? 0 : ViewConfiguration.getDoubleTapTimeout();
uiHandler.postDelayed(hideAllInputLayoutRunnable, delay);
}
/**
* ****************************** 语音 ***********************************
*/
private void initAudioRecordButton() {
audioRecordBtn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
touched = true;
initAudioRecord();
onStartAudioRecord();
} else if (event.getAction() == MotionEvent.ACTION_CANCEL
|| event.getAction() == MotionEvent.ACTION_UP) {
touched = false;
onEndAudioRecord(isCancelled(v, event));
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
touched = true;
cancelAudioRecord(isCancelled(v, event));
}
return false;
}
});
}
// 上滑取消录音判断
private static boolean isCancelled(View view, MotionEvent event) {
int[] location = new int[2];
view.getLocationOnScreen(location);
return event.getRawX() < location[0] || event.getRawX() > location[0] + view.getWidth()
|| event.getRawY() < location[1] - 40;
}
/**
* 初始化AudioRecord
*/
private void initAudioRecord() {
if (audioMessageHelper == null) {
UIKitOptions options = NimUIKitImpl.getOptions();
audioMessageHelper = new AudioRecorder(container.activity, options.audioRecordType, options.audioRecordMaxTime, this);
}
}
/**
* 开始语音录制
*/
private void onStartAudioRecord() {
container.activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
audioMessageHelper.startRecord();
cancelled = false;
}
/**
* 结束语音录制
*
* @param cancel
*/
private void onEndAudioRecord(boolean cancel) {
started = false;
container.activity.getWindow().setFlags(0, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
audioMessageHelper.completeRecord(cancel);
audioRecordBtn.setText(R.string.record_audio);
audioRecordBtn.setBackgroundResource(R.drawable.bg_message_voice);
stopAudioRecordAnim();
if (!cancel) {
EventBus.getDefault().post(new ActiveEvent());
}
}
/**
* 取消语音录制
*
* @param cancel
*/
private void cancelAudioRecord(boolean cancel) {
// reject
if (!started) {
return;
}
// no change
if (cancelled == cancel) {
return;
}
cancelled = cancel;
updateTimerTip(cancel);
}
/**
* 正在进行语音录制和取消语音录制,界面展示
*
* @param cancel
*/
private void updateTimerTip(boolean cancel) {
if (cancel) {
timerTip.setText(R.string.recording_cancel_tip);
timerTipContainer.setBackgroundResource(R.drawable.nim_cancel_record_red_bg);
} else {
timerTip.setText(R.string.recording_cancel);
timerTipContainer.setBackgroundResource(0);
}
}
/**
* 开始语音录制动画
*/
private void playAudioRecordAnim() {
audioAnimLayout.setVisibility(View.VISIBLE);
time.setBase(SystemClock.elapsedRealtime());
time.start();
}
/**
* 结束语音录制动画
*/
private void stopAudioRecordAnim() {
audioAnimLayout.setVisibility(View.GONE);
time.stop();
time.setBase(SystemClock.elapsedRealtime());
}
// 录音状态回调
@Override
public void onRecordReady() {
}
@Override
public void onRecordStart(File audioFile, RecordType recordType) {
started = true;
if (!touched) {
return;
}
audioRecordBtn.setText(R.string.record_audio_end);
audioRecordBtn.setBackgroundResource(R.drawable.bg_message_voice_pressed);
updateTimerTip(false); // 初始化语音动画状态
playAudioRecordAnim();
}
@Override
public void onRecordSuccess(File audioFile, long audioLength, RecordType recordType) {
IMMessage audioMessage = ChatRoomMessageBuilder.createChatRoomAudioMessage(container.account, audioFile, audioLength);
container.proxy.sendMessage(audioMessage);
}
@Override
public void onRecordFail() {
if (started) {
// Toast.makeText(container.activity, R.string.recording_error, Toast.LENGTH_SHORT).show();
SingleToastUtil.showToastShort(R.string.recording_error);
}
}
@Override
public void onRecordCancel() {
}
@Override
public void onRecordReachedMaxTime(final int maxTime) {
stopAudioRecordAnim();
EasyAlertDialogHelper.createOkCancelDiolag(container.activity, "", container.activity.getString(R.string.recording_max_time), false, new EasyAlertDialogHelper.OnDialogActionListener() {
@Override
public void doCancelAction() {
}
@Override
public void doOkAction() {
audioMessageHelper.handleEndRecord(true, maxTime);
}
}).show();
}
public boolean isRecording() {
return audioMessageHelper != null && audioMessageHelper.isRecording();
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
int index = (requestCode << 16) >> 24;
if (index != 0) {
index--;
if (index < 0 | index >= actions.size()) {
LogUtil.d(TAG, "request code out of actions' range");
return;
}
BaseAction action = actions.get(index);
if (action != null) {
action.onActivityResult(requestCode & 0xff, resultCode, data);
}
}
}
public void switchRobotMode(boolean isRobot) {
isRobotSession = isRobot;
if (isRobot) {
textAudioSwitchLayout.setVisibility(View.GONE);
emojiButtonInInputBar.setVisibility(View.GONE);
sendMessageButtonInInputBar.setVisibility(View.VISIBLE);
moreFuntionButtonInInputBar.setVisibility(View.GONE);
} else {
textAudioSwitchLayout.setVisibility(isTextAudioSwitchShow ? View.VISIBLE : View.GONE);
// textAudioSwitchLayout.setVisibility(View.VISIBLE);
emojiButtonInInputBar.setVisibility(View.VISIBLE);
sendMessageButtonInInputBar.setVisibility(View.GONE);
moreFuntionButtonInInputBar.setVisibility(View.VISIBLE);
}
}
public void reloadActions(List<BaseAction> actions) {
this.actions = actions;
for (int i = 0; i < this.actions.size(); ++i) {
this.actions.get(i).setIndex(i);
this.actions.get(i).setContainer(container);
}
ActionsPanel.init(view, actions);
}
private int moreCustomId = -1;
public void setMoreFuntionButtonInInputBar(int drawable, int titleId) {
isCustomOption = true;
moreFuntionButtonInInputBar.setBackgroundResource(drawable);
moreCustomId = titleId;
}
public void sendCustomMessage(IMMessage customMessage) {
container.proxy.sendMessage(customMessage);
}
public void setLimitLevel(boolean isChat, String msg) {
this.isChat = isChat;
setEditTextState();
}
private void setEditTextState() {
if (!isChat) {
messageEditText.setHint(ResUtil.getString(R.string.module_input_inputpanel_06));
messageEditText.setFocusable(false);
messageEditText.setFocusableInTouchMode(false);
messageEditText.setEnabled(false);
moreFuntionButtonInInputBar.setEnabled(false);
sendMessageButtonInInputBar.setEnabled(false);
} else {
messageEditText.setHint(ResUtil.getString(R.string.module_input_inputpanel_07));
messageEditText.setFocusable(true);
messageEditText.setFocusableInTouchMode(true);
messageEditText.setEnabled(true);
moreFuntionButtonInInputBar.setEnabled(true);
sendMessageButtonInInputBar.setEnabled(true);
}
}
}

View File

@@ -0,0 +1,94 @@
package com.chwl.app.public_chat.ui.message
import android.content.Context
import android.content.Intent
import androidx.activity.viewModels
import androidx.core.view.isVisible
import com.chwl.app.R
import com.chwl.app.base.BaseBindingActivity
import com.chwl.app.databinding.PublicChatMessageActivityBinding
import com.chwl.app.public_chat.ui.message.headline.HeadlineSendDialog
import com.chwl.app.ui.utils.loadAvatar
import com.chwl.core.initial.InitialModel
import com.chwl.core.public_chat_hall.bean.HeadlineBean
import com.chwl.library.annatation.ActLayoutRes
import com.chwl.library.utils.ResUtil
import com.chwl.library.utils.SingleToastUtil
import com.example.lib_utils.ktx.singleClick
import com.example.lib_utils.log.ILog
@ActLayoutRes(R.layout.public_chat_message_activity)
class PublicChatRoomMessageActivity : BaseBindingActivity<PublicChatMessageActivityBinding>(),
ILog {
private val viewModel by viewModels<PublicChatRoomViewModel>()
companion object {
fun start(context: Context): Boolean {
val sessionId =
InitialModel.get().publicChatSessionId
if (sessionId.isNullOrEmpty()) {
SingleToastUtil.showToast(R.string.public_chat_not_found)
return false
} else {
val intent = Intent(context, PublicChatRoomMessageActivity::class.java)
intent.putExtra("sessionId", sessionId)
context.startActivity(intent)
return true
}
}
}
override fun init() {
val sessionId = intent.getStringExtra("sessionId")
if (sessionId.isNullOrEmpty()) {
finish()
return
}
initView(sessionId)
initEvent()
initObserve()
}
private fun initView(sessionId: String) {
initWhiteTitleBar(ResUtil.getString(R.string.public_chat_room))
val fragment = PublicChatRoomMessageFragment.newInstance(sessionId)
supportFragmentManager.beginTransaction().add(R.id.fragment_message, fragment)
.commitAllowingStateLoss()
}
private fun initEvent() {
mBinding?.layoutHeadlineAdd?.singleClick {
HeadlineSendDialog().show(supportFragmentManager, "HEADLINE_SEND")
}
mBinding?.ivHeadline?.singleClick {
HeadlineSendDialog().show(supportFragmentManager, "HEADLINE_SEND")
}
}
private fun initObserve() {
viewModel.headlineLiveData.observe(this) {
mBinding.tvHeadlineContent.text = it?.content ?: ""
mBinding.ivHeadlineUserAvatar.loadAvatar(it?.avatar)
mBinding.tvHeadlineUserName.text = "${it?.nick ?: ""} :"
mBinding.tvHeadlineMoney.text = it?.payMoneyNum?.toString() ?: "0"
if (it?.content.isNullOrEmpty()) {
mBinding.layoutHeadline.isVisible = false
mBinding.layoutHeadlineAdd.isVisible = true
} else {
mBinding.layoutHeadline.isVisible = true
mBinding.layoutHeadlineAdd.isVisible = false
}
}
viewModel.getCurrentHeadline()
}
override fun onBackPressed() {
val messageFragment = (supportFragmentManager.fragments.firstOrNull {
it is PublicChatRoomMessageFragment
} as? PublicChatRoomMessageFragment)
if (messageFragment == null || !messageFragment.onBackPressed()) {
super.onBackPressed()
}
}
}

View File

@@ -0,0 +1,177 @@
package com.chwl.app.public_chat.ui.message
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.activityViewModels
import com.chwl.app.R
import com.chwl.app.base.BaseBindingFragment
import com.chwl.app.databinding.PublicChatMessageFragmentBinding
import com.chwl.library.annatation.ActLayoutRes
import com.chwl.library.utils.SingleToastUtil
import com.example.lib_utils.log.ILog
import com.netease.nim.uikit.api.model.NimException
import com.netease.nim.uikit.business.session.actions.BaseAction
import com.netease.nim.uikit.business.session.actions.ImageAction
import com.netease.nim.uikit.business.session.module.Container
import com.netease.nim.uikit.business.session.module.ModuleProxy
import com.netease.nimlib.sdk.NIMChatRoomSDK
import com.netease.nimlib.sdk.Observer
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
import com.netease.nimlib.sdk.chatroom.model.ChatRoomStatusChangeData
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum
import com.netease.nimlib.sdk.msg.model.IMMessage
@ActLayoutRes(R.layout.public_chat_message_fragment)
class PublicChatRoomMessageFragment : BaseBindingFragment<PublicChatMessageFragmentBinding>(),
ModuleProxy, ILog {
private val viewModel by activityViewModels<PublicChatRoomViewModel>()
private val chatRoomClient get() = viewModel.chatRoomClient
private var sessionId = "-1"
private var inputPanel: PublicChatRoomInputPanel? = null
private var messageListPanel: PublicChatRoomMessageListPanel? = null
companion object {
fun newInstance(sessionId: String): PublicChatRoomMessageFragment {
return PublicChatRoomMessageFragment().apply {
arguments = Bundle().apply {
putString("sessionId", sessionId)
}
}
}
}
override fun initiate() {
val sessionId = arguments?.getString("sessionId")
init(sessionId ?: "-1")
}
@SuppressLint("CheckResult")
private fun init(sessionId: String) {
this.sessionId = sessionId
viewModel.init(sessionId)
val container = Container(activity, sessionId, SessionTypeEnum.ChatRoom, this)
messageListPanel =
PublicChatRoomMessageListPanel(
container,
mBinding.root
)
inputPanel =
PublicChatRoomInputPanel(
container,
mBinding.root,
getActionList()
)
inputPanel?.setLimitLevel(true, "")
registerObservers(true)
chatRoomClient?.enterChatRoom()?.compose(bindToLifecycle())
?.subscribe({ messageListPanel?.requestHistory() },
{
if (it is NimException) {
toast(getString(R.string.avroom_fragment_homepartyroomfragment_011) + "(${it.code})")
} else {
toast(R.string.avroom_fragment_homepartyroomfragment_011)
}
})
}
override fun onPause() {
super.onPause()
messageListPanel?.onPause()
}
override fun onResume() {
super.onResume()
messageListPanel?.onResume()
}
override fun sendMessage(msg: IMMessage?): Boolean {
val message = msg as? ChatRoomMessage ?: return false
viewModel.sendMessage(message)
messageListPanel?.onMsgSend(message)
return true
}
override fun onInputPanelExpand() {
messageListPanel?.scrollToBottom()
}
override fun shouldCollapseInputPanel() {
inputPanel?.collapse(false)
}
override fun isLongClickEnabled(): Boolean {
return !(inputPanel?.isRecording ?: false)
}
override fun onItemFooterClick(message: IMMessage?) {
}
override fun onAudioClick(runnable: Runnable?) {
checkPermission(Manifest.permission.RECORD_AUDIO)
.subscribe { result: Boolean ->
if (result) {
runnable?.run()
} else {
SingleToastUtil.showToast(getString(R.string.ask_again))
}
}
}
fun onBackPressed(): Boolean {
if (inputPanel?.collapse(true) == true) {
return true
}
return messageListPanel?.onBackPressed() == true
}
// 操作面板集合
private fun getActionList(): List<BaseAction> {
val actions: MutableList<BaseAction> = ArrayList()
actions.add(ImageAction())
return actions
}
private fun registerObservers(register: Boolean) {
NIMChatRoomSDK.getChatRoomServiceObserve()
.observeReceiveMessage(incomingMessageObserver, register)
NIMChatRoomSDK.getChatRoomServiceObserve()
.observeOnlineStatus(onlineStatusObserver, register)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
inputPanel?.onActivityResult(requestCode, resultCode, data)
messageListPanel?.onActivityResult(requestCode, resultCode, data)
}
override fun onDestroy() {
super.onDestroy()
registerObservers(false)
messageListPanel?.onDestroy()
}
/**
* 消息接收观察者
*/
private var incomingMessageObserver: Observer<List<ChatRoomMessage>> =
Observer<List<ChatRoomMessage>> { messages ->
if (messages == null || messages.isEmpty()) {
return@Observer
}
messageListPanel?.onIncomingMessage(messages)
}
private val onlineStatusObserver: Observer<ChatRoomStatusChangeData> =
Observer<ChatRoomStatusChangeData>
{
logI("onlineStatusObserver roomId:${it.roomId} status:${it.status}")
if (it.roomId == sessionId) {
if (it.status.wontAutoLogin()) {
toast(R.string.xchat_android_core_manager_imneteasemanager_012)
}
}
}
}

View File

@@ -0,0 +1,863 @@
package com.chwl.app.public_chat.ui.message;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.text.TextUtils;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.chwl.app.R;
import com.chwl.app.public_chat.core.ChatRoomClientManager;
import com.chwl.app.public_chat.core.ChatRoomMessageAdapter;
import com.chwl.core.im.custom.bean.HeadlineChangedAttachment;
import com.chwl.core.module_hall.im.HallAttachment;
import com.chwl.core.module_hall.im.HallImMsgInfo;
import com.chwl.core.public_chat_hall.bean.HeadlineBean;
import com.chwl.library.utils.ResUtil;
import com.chwl.library.utils.SingleToastUtil;
import com.netease.nim.uikit.api.NimUIKit;
import com.netease.nim.uikit.business.preference.UserPreferences;
import com.netease.nim.uikit.business.robot.parser.elements.group.LinkElement;
import com.netease.nim.uikit.business.session.activity.VoiceTrans;
import com.netease.nim.uikit.business.session.audio.MessageAudioControl;
import com.netease.nim.uikit.business.session.module.Container;
import com.netease.nim.uikit.business.session.viewholder.robot.RobotLinkView;
import com.netease.nim.uikit.common.ui.dialog.CustomAlertDialog;
import com.netease.nim.uikit.common.ui.dialog.EasyAlertDialog;
import com.netease.nim.uikit.common.ui.dialog.EasyAlertDialogHelper;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.BaseFetchLoadAdapter;
import com.netease.nim.uikit.common.ui.recyclerview.adapter.IRecyclerView;
import com.netease.nim.uikit.common.ui.recyclerview.listener.OnItemClickListener;
import com.netease.nim.uikit.common.ui.recyclerview.loadmore.MsgListFetchLoadMoreView;
import com.netease.nim.uikit.common.util.AntiSpamUtil;
import com.netease.nim.uikit.common.util.sys.ClipboardUtil;
import com.netease.nim.uikit.impl.NimUIKitImpl;
import com.netease.nimlib.sdk.NIMClient;
import com.netease.nimlib.sdk.Observer;
import com.netease.nimlib.sdk.RequestCallback;
import com.netease.nimlib.sdk.RequestCallbackWrapper;
import com.netease.nimlib.sdk.ResponseCode;
import com.netease.nimlib.sdk.chatroom.ChatRoomMessageBuilder;
import com.netease.nimlib.sdk.chatroom.ChatRoomService;
import com.netease.nimlib.sdk.chatroom.ChatRoomServiceObserver;
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage;
import com.netease.nimlib.sdk.msg.MsgService;
import com.netease.nimlib.sdk.msg.attachment.FileAttachment;
import com.netease.nimlib.sdk.msg.constant.AttachStatusEnum;
import com.netease.nimlib.sdk.msg.constant.MsgDirectionEnum;
import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum;
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum;
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum;
import com.netease.nimlib.sdk.msg.model.AttachmentProgress;
import com.netease.nimlib.sdk.msg.model.IMMessage;
import com.netease.nimlib.sdk.msg.model.QueryDirectionEnum;
import com.netease.nimlib.sdk.robot.model.RobotAttachment;
import com.netease.nimlib.sdk.robot.model.RobotMsgType;
import com.netease.nimlib.sdk.team.constant.TeamMemberType;
import com.netease.nimlib.sdk.team.model.TeamMember;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* 基于RecyclerView的消息收发模块
* Created by huangjun on 2016/12/27.
*/
public class PublicChatRoomMessageListPanel {
private static Comparator<ChatRoomMessage> comp = new Comparator<ChatRoomMessage>() {
@Override
public int compare(ChatRoomMessage o1, ChatRoomMessage o2) {
long time = o1.getTime() - o2.getTime();
return time == 0 ? 0 : (time < 0 ? -1 : 1);
}
};
// container
private Container container;
private View rootView;
// message list view
private RecyclerView messageListView;
private List<ChatRoomMessage> items;
private ChatRoomMessageAdapter adapter;
private Handler uiHandler;
// 语音转文字
private VoiceTrans voiceTrans;
private MessageLoader messageLoader;
private int maxMessageCount = 1000;
private OnItemClickListener listener = new OnItemClickListener() {
@Override
public void onItemClick(IRecyclerView adapter, View view, int position) {
}
@Override
public void onItemLongClick(IRecyclerView adapter, View view, int position) {
}
@Override
public void onItemChildClick(IRecyclerView adapter2, View view, int position) {
if (view != null && view instanceof RobotLinkView) {
RobotLinkView robotLinkView = (RobotLinkView) view;
// robotLinkView.onClick();
LinkElement element = robotLinkView.getElement();
if (element != null) {
element.getTarget();
if (LinkElement.TYPE_URL.equals(element.getType())) {
Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
Uri content_url = Uri.parse(element.getTarget());
intent.setData(content_url);
try {
container.activity.startActivity(intent);
} catch (ActivityNotFoundException e) {
// Toast.makeText(container.activity, ResUtil.getString(R.string.ui_im_messagelistpanelex_01), Toast.LENGTH_SHORT).show();
SingleToastUtil.showToastShort(ResUtil.getString(R.string.ui_im_messagelistpanelex_02));
}
} else if (LinkElement.TYPE_BLOCK.equals(element.getType())) {
// 发送点击的block
ChatRoomMessage message = adapter.getItem(position);
if (message != null) {
String robotAccount = ((RobotAttachment) message.getAttachment()).getFromRobotAccount();
ChatRoomMessage robotMsg = ChatRoomMessageBuilder.createRobotMessage(message.getSessionId(), robotAccount,
robotLinkView.getShowContent(), RobotMsgType.LINK, "", element.getTarget(), element.getParams());
container.proxy.sendMessage(robotMsg);
}
}
}
}
}
};
/**
* 消息状态变化观察者
*/
private Observer<ChatRoomMessage> messageStatusObserver = new Observer<ChatRoomMessage>() {
@Override
public void onEvent(ChatRoomMessage message) {
if (isMyMessage(message)) {
onMessageStatusChange(message);
}
}
};
/**
* 消息附件上传/下载进度观察者
*/
private Observer<AttachmentProgress> attachmentProgressObserver = new Observer<AttachmentProgress>() {
@Override
public void onEvent(AttachmentProgress progress) {
onAttachmentProgressChange(progress);
}
};
public PublicChatRoomMessageListPanel(Container container, View rootView) {
this.container = container;
this.rootView = rootView;
init();
}
public void onResume() {
setEarPhoneMode(UserPreferences.isEarPhoneModeEnable(), false);
}
public void onPause() {
MessageAudioControl.getInstance(container.activity).stopAudio();
}
public void onDestroy() {
registerObservers(false);
}
public boolean onBackPressed() {
uiHandler.removeCallbacks(null);
MessageAudioControl.getInstance(container.activity).stopAudio(); // 界面返回,停止语音播放
if (voiceTrans != null && voiceTrans.isShow()) {
voiceTrans.hide();
return true;
}
return false;
}
public void reload(Container container, ChatRoomMessage anchor) {
this.container = container;
if (adapter != null) {
adapter.clearData();
}
initFetchLoadListener();
}
private void init() {
initListView();
this.uiHandler = new Handler();
registerObservers(true);
}
private void initListView() {
// RecyclerView
messageListView = (RecyclerView) rootView.findViewById(R.id.messageListView);
messageListView.setLayoutManager(new LinearLayoutManager(container.activity));
messageListView.requestDisallowInterceptTouchEvent(true);
messageListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState != RecyclerView.SCROLL_STATE_IDLE) {
container.proxy.shouldCollapseInputPanel();
}
}
});
messageListView.setOverScrollMode(View.OVER_SCROLL_NEVER);
// adapter
items = new ArrayList<>();
adapter = new ChatRoomMessageAdapter(messageListView, items, container);
adapter.setFetchMoreView(new MsgListFetchLoadMoreView());
adapter.setLoadMoreView(new MsgListFetchLoadMoreView());
adapter.setEventListener(new MsgItemEventListener());
initFetchLoadListener();
messageListView.setAdapter(adapter);
messageListView.addOnItemTouchListener(listener);
}
private void initFetchLoadListener() {
this.messageLoader = new MessageLoader();
adapter.setOnFetchMoreListener(messageLoader);
}
public void requestHistory(){
if (messageLoader != null) {
messageLoader.loadFromRemote();
}
}
// 刷新消息列表
public void refreshMessageList() {
container.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
}
});
}
public void scrollToBottom() {
uiHandler.postDelayed(new Runnable() {
@Override
public void run() {
doScrollToBottom();
}
}, 200);
}
private void doScrollToBottom() {
messageListView.scrollToPosition(adapter.getBottomDataPosition());
}
public void onIncomingMessage(List<ChatRoomMessage> messages) {
boolean needScrollToBottom = isLastMessageVisible();
boolean needRefresh = false;
List<ChatRoomMessage> addedListItems = new ArrayList<>(messages.size());
for (ChatRoomMessage message : messages) {
if (isMyMessage(message) && filterMessage(message)) {
items.add(message);
addedListItems.add(message);
needRefresh = true;
}
}
if (needRefresh) {
sortMessages(items);
if (items.size() > maxMessageCount) {
for (int i = 0; i < maxMessageCount / 3; i++) {
items.remove(0);
}
}
adapter.notifyDataSetChanged();
}
adapter.updateShowTimeItem(addedListItems, false, true);
// incoming messages tip
ChatRoomMessage lastMsg = messages.get(messages.size() - 1);
if (isMyMessage(lastMsg)) {
if (needScrollToBottom) {
doScrollToBottom();
}
}
}
private boolean isLastMessageVisible() {
LinearLayoutManager layoutManager = (LinearLayoutManager) messageListView.getLayoutManager();
int lastVisiblePosition = layoutManager.findLastCompletelyVisibleItemPosition();
return lastVisiblePosition >= adapter.getBottomDataPosition();
}
// 发送消息后,更新本地消息列表
public void onMsgSend(ChatRoomMessage message) {
List<ChatRoomMessage> addedListItems = new ArrayList<>(1);
addedListItems.add(message);
adapter.updateShowTimeItem(addedListItems, false, true);
adapter.appendData(message);
doScrollToBottom();
}
/**
* **************************** 排序 ***********************************
*/
private void sortMessages(List<ChatRoomMessage> list) {
if (list.size() == 0) {
return;
}
Collections.sort(list, comp);
}
/**
* ************************* 观察者 ********************************
*/
private void registerObservers(boolean register) {
ChatRoomServiceObserver service = NIMClient.getService(ChatRoomServiceObserver.class);
service.observeMsgStatus(messageStatusObserver, register);
service.observeAttachmentProgress(attachmentProgressObserver, register);
}
private void onMessageStatusChange(ChatRoomMessage message) {
int index = getItemIndex(message.getUuid());
if (index >= 0 && index < items.size()) {
ChatRoomMessage item = items.get(index);
if (AntiSpamUtil.isAntiSpam(message)) {
item.setStatus(MsgStatusEnum.fail);
NIMClient.getService(MsgService.class).updateIMMessageStatus(item);
} else {
item.setStatus(message.getStatus());
}
item.setAttachStatus(message.getAttachStatus());
// 处理语音、音视频通话
if (item.getMsgType() == MsgTypeEnum.audio || item.getMsgType() == MsgTypeEnum.avchat) {
item.setAttachment(message.getAttachment()); // 附件可能更新了
}
// resend的的情况可能时间已经变化了这里要重新检查是否要显示时间
List<ChatRoomMessage> msgList = new ArrayList<>(1);
msgList.add(message);
adapter.updateShowTimeItem(msgList, false, true);
refreshViewHolderByIndex(index);
}
}
private void onAttachmentProgressChange(AttachmentProgress progress) {
int index = getItemIndex(progress.getUuid());
if (index >= 0 && index < items.size()) {
ChatRoomMessage item = items.get(index);
float value = (float) progress.getTransferred() / (float) progress.getTotal();
adapter.putProgress(item, value);
refreshViewHolderByIndex(index);
}
}
public boolean isMyMessage(ChatRoomMessage message) {
return message.getSessionType() == container.sessionType
&& message.getSessionId() != null
&& message.getSessionId().equals(container.account);
}
public boolean filterMessage(ChatRoomMessage message) {
if (ChatRoomClientManager.INSTANCE.getPublicChatRoomReceiveMessageFilter().filter(message)) {
if (message.getMsgType() == MsgTypeEnum.custom && message.getAttachment() instanceof HeadlineChangedAttachment) {
HeadlineBean data = ((HeadlineChangedAttachment) message.getAttachment()).getHeadlineData();
if (data == null || !data.isValid()) {
return false;
}
}
return true;
}
return false;
}
/**
* 刷新单条消息
*/
public void refreshViewHolderByIndex(final int index) {
container.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (index < 0) {
return;
}
adapter.notifyDataItemChanged(index);
}
});
}
public int getItemIndex(String uuid) {
for (int i = 0; i < items.size(); i++) {
ChatRoomMessage message = items.get(i);
if (TextUtils.equals(message.getUuid(), uuid)) {
return i;
}
}
return -1;
}
public int getApplyItemIndex(String recordId) {
for (int i = 0; i < items.size(); i++) {
ChatRoomMessage message = items.get(i);
if (message.getMsgType() == MsgTypeEnum.custom && message.getAttachment() != null) {
if (message.getAttachment() instanceof HallAttachment) {
HallAttachment attachment = (HallAttachment) message.getAttachment();
HallImMsgInfo info = attachment.getHallImMsgInfo();
if (info != null && info.getUrl() != null && info.getUrl().contains("recordId")) {
Uri uri = Uri.parse(info.getUrl());
String record = uri.getQueryParameter("recordId");
if (recordId.equals(record)) {
return i;
}
}
}
}
}
return -1;
}
private void setEarPhoneMode(boolean earPhoneMode, boolean update) {
if (update) {
UserPreferences.setEarPhoneModeEnable(earPhoneMode);
}
MessageAudioControl.getInstance(container.activity).setEarPhoneModeEnable(earPhoneMode);
}
public void updateReceipt(final List<ChatRoomMessage> messages) {
for (int i = messages.size() - 1; i >= 0; i--) {
if (receiveReceiptCheck(messages.get(i))) {
adapter.setUuid(messages.get(i).getUuid());
break;
}
}
}
private boolean receiveReceiptCheck(final ChatRoomMessage msg) {
if (msg != null && msg.getSessionType() == SessionTypeEnum.P2P
&& msg.getDirect() == MsgDirectionEnum.Out
&& msg.getMsgType() != MsgTypeEnum.tip
&& msg.getMsgType() != MsgTypeEnum.notification
&& msg.isRemoteRead()) {
return true;
}
return false;
}
/**
* 发送已读回执(需要过滤)
*/
public void sendReceipt() {
// 查询全局已读回执功能开关配置
if (!NimUIKitImpl.getOptions().shouldHandleReceipt) {
return;
}
if (container.account == null || container.sessionType != SessionTypeEnum.P2P) {
return;
}
ChatRoomMessage message = getLastReceivedMessage();
if (!sendReceiptCheck(message)) {
return;
}
NIMClient.getService(MsgService.class).sendMessageReceipt(container.account, message);
}
private ChatRoomMessage getLastReceivedMessage() {
ChatRoomMessage lastMessage = null;
for (int i = items.size() - 1; i >= 0; i--) {
if (sendReceiptCheck(items.get(i))) {
lastMessage = items.get(i);
break;
}
}
return lastMessage;
}
private boolean sendReceiptCheck(final ChatRoomMessage msg) {
if (msg == null || msg.getDirect() != MsgDirectionEnum.In ||
msg.getMsgType() == MsgTypeEnum.tip || msg.getMsgType() == MsgTypeEnum.notification) {
return false; // 非收到的消息Tip消息和通知类消息不要发已读回执
}
return true;
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
/**
* ***************************************** 数据加载 *********************************************
*/
private class MessageLoader implements BaseFetchLoadAdapter.RequestLoadMoreListener, BaseFetchLoadAdapter.RequestFetchMoreListener {
private int loadMsgCount = 100;
private int historyMaxCount = 200;
private ChatRoomMessage anchor;
private boolean firstLoad = true;
private boolean fetching = false;
private RequestCallback<List<ChatRoomMessage>> callback = new RequestCallbackWrapper<List<ChatRoomMessage>>() {
@Override
public void onResult(int code, List<ChatRoomMessage> messages, Throwable exception) {
if (code == ResponseCode.RES_SUCCESS && messages != null) {
onMessageLoaded(messages);
} else {
adapter.fetchMoreFailed();
}
fetching = false;
}
};
public MessageLoader() {
}
public synchronized void loadFromRemote() {
if (fetching) {
return;
}
fetching = true;
MsgTypeEnum[] typeEnums = new MsgTypeEnum[]{MsgTypeEnum.text, MsgTypeEnum.image, MsgTypeEnum.audio};
NIMClient.getService(ChatRoomService.class).pullMessageHistoryExType(container.account, anchor().getTime(), loadMsgCount, QueryDirectionEnum.QUERY_OLD, typeEnums)
.setCallback(callback);
}
private ChatRoomMessage anchor() {
if (items.size() == 0) {
return anchor == null ? ChatRoomMessageBuilder.createEmptyChatRoomMessage(container.account, 0) : anchor;
} else {
return items.get(0);
}
}
/**
* 私聊聊天信息数(发起私聊限制需要用到)
*
* @param messages
*/
private void onMessageLoaded(List<ChatRoomMessage> messages) {
if (messages == null) {
return;
}
boolean noMoreMessage = messages.size() < loadMsgCount;
Collections.reverse(messages);
// 在第一次加载的过程中又收到了新消息,做一下去重
if (firstLoad && items.size() > 0) {
for (ChatRoomMessage message : messages) {
int removeIndex = 0;
for (ChatRoomMessage item : items) {
if (item.isTheSame(message)) {
adapter.remove(removeIndex);
break;
}
removeIndex++;
}
}
}
// 加入anchor
if (firstLoad && anchor != null) {
messages.add(anchor);
}
// 在更新前,先确定一些标记
List<ChatRoomMessage> total = new ArrayList<>();
total.addAll(items);
total.addAll(0, messages);
adapter.updateShowTimeItem(total, true, firstLoad); // 更新要显示时间的消息
updateReceipt(total); // 更新已读回执标签
if (noMoreMessage || total.size() >= historyMaxCount) {
adapter.fetchMoreEnd(messages, true);
} else {
adapter.fetchMoreComplete(messages);
}
// 如果是第一次加载updateShowTimeItem返回的就是lastShowTimeItem
if (firstLoad) {
doScrollToBottom();
sendReceipt(); // 发送已读回执
}
firstLoad = false;
}
@Override
public void onFetchMoreRequested() {
loadFromRemote();
}
@Override
public void onLoadMoreRequested() {
}
}
private class MsgItemEventListener implements ChatRoomMessageAdapter.ViewHolderEventListener {
@Override
public void onFailedBtnClick(IMMessage message) {
if (message.getDirect() == MsgDirectionEnum.Out) {
// 发出的消息,如果是发送失败,直接重发,否则有可能是漫游到的多媒体消息,但文件下载
if (message.getStatus() == MsgStatusEnum.fail) {
if (AntiSpamUtil.isAntiSpam(message)) {
return;
}
resendMessage(message); // 重发
} else {
if (message.getAttachment() instanceof FileAttachment) {
FileAttachment attachment = (FileAttachment) message.getAttachment();
if (TextUtils.isEmpty(attachment.getPath())
&& TextUtils.isEmpty(attachment.getThumbPath())) {
showReDownloadConfirmDlg(message);
}
} else {
resendMessage(message);
}
}
} else {
showReDownloadConfirmDlg(message);
}
}
@Override
public boolean onViewHolderLongClick(View clickView, View viewHolderView, IMMessage item) {
if (container.proxy.isLongClickEnabled()) {
showLongClickAction(item);
}
return true;
}
@Override
public void onFooterClick(IMMessage message) {
// 与 robot 对话
container.proxy.onItemFooterClick(message);
}
// 重新下载(对话框提示)
private void showReDownloadConfirmDlg(final IMMessage message) {
EasyAlertDialogHelper.OnDialogActionListener listener = new EasyAlertDialogHelper.OnDialogActionListener() {
@Override
public void doCancelAction() {
}
@Override
public void doOkAction() {
// 正常情况收到消息后附件会自动下载。如果下载失败,可调用该接口重新下载
if (message.getAttachment() != null && message.getAttachment() instanceof FileAttachment)
NIMClient.getService(MsgService.class).downloadAttachment(message, true);
}
};
final EasyAlertDialog dialog = EasyAlertDialogHelper.createOkCancelDiolag(container.activity, null,
container.activity.getString(R.string.repeat_download_message), true, listener);
dialog.show();
}
// 重发消息到服务器
private void resendMessage(IMMessage message) {
// 重置状态为unsent
int index = getItemIndex(message.getUuid());
if (index >= 0 && index < items.size()) {
ChatRoomMessage item = items.get(index);
item.setStatus(MsgStatusEnum.sending);
refreshViewHolderByIndex(index);
}
NIMClient.getService(MsgService.class)
.sendMessage(message, true)
.setCallback(new RequestCallbackWrapper<Void>() {
@Override
public void onResult(int code, Void result, Throwable exception) {
}
});
}
/**
* ****************************** 长按菜单 ********************************
*/
// 长按消息Item后弹出菜单控制
private void showLongClickAction(IMMessage selectedItem) {
onNormalLongClick(selectedItem);
}
/**
* 长按菜单操作
*
* @param item
*/
private void onNormalLongClick(IMMessage item) {
CustomAlertDialog alertDialog = new CustomAlertDialog(container.activity);
alertDialog.setCancelable(true);
alertDialog.setCanceledOnTouchOutside(true);
prepareDialogItems(item, alertDialog);
alertDialog.show();
}
// 长按消息item的菜单项准备。如果消息item的MsgViewHolder处理长按事件(MsgViewHolderBase#onItemLongClick),且返回为true
// 则对应项的长按事件不会调用到此处
private void prepareDialogItems(final IMMessage selectedItem, CustomAlertDialog alertDialog) {
MsgTypeEnum msgType = selectedItem.getMsgType();
MessageAudioControl.getInstance(container.activity).stopAudio();
// 0 EarPhoneMode
longClickItemEarPhoneMode(alertDialog, msgType);
// 1 resend
longClickItemResend(selectedItem, alertDialog);
// 2 copy
longClickItemCopy(selectedItem, alertDialog, msgType);
// 5 trans
longClickItemVoidToText(selectedItem, alertDialog, msgType);
}
private boolean enableRevokeButton(IMMessage selectedItem) {
if (selectedItem.getStatus() == MsgStatusEnum.success
&& NimUIKitImpl.getMsgRevokeFilter() != null
&& !NimUIKitImpl.getMsgRevokeFilter().shouldIgnore(selectedItem)) {
if (selectedItem.getDirect() == MsgDirectionEnum.Out) {
return true;
} else if (NimUIKit.getOptions().enableTeamManagerRevokeMsg && selectedItem.getSessionType() == SessionTypeEnum.Team) {
TeamMember member = NimUIKit.getTeamProvider().getTeamMember(selectedItem.getSessionId(), NimUIKit.getAccount());
return member != null && member.getType() == TeamMemberType.Owner || member.getType() == TeamMemberType.Manager;
}
}
return false;
}
// 长按菜单项--重发
private void longClickItemResend(final IMMessage item, CustomAlertDialog alertDialog) {
if (item.getStatus() != MsgStatusEnum.fail) {
return;
}
alertDialog.addItem(container.activity.getString(R.string.repeat_send_has_blank), new CustomAlertDialog.onSeparateItemClickListener() {
@Override
public void onClick() {
onResendMessageItem(item);
}
});
}
private void onResendMessageItem(IMMessage message) {
int index = getItemIndex(message.getUuid());
if (index >= 0) {
showResendConfirm(message, index); // 重发确认
}
}
private void showResendConfirm(final IMMessage message, final int index) {
EasyAlertDialogHelper.OnDialogActionListener listener = new EasyAlertDialogHelper.OnDialogActionListener() {
@Override
public void doCancelAction() {
}
@Override
public void doOkAction() {
resendMessage(message);
}
};
final EasyAlertDialog dialog = EasyAlertDialogHelper.createOkCancelDiolag(container.activity, null,
container.activity.getString(R.string.repeat_send_message), true, listener);
dialog.show();
}
// 长按菜单项--复制
private void longClickItemCopy(final IMMessage item, CustomAlertDialog alertDialog, MsgTypeEnum msgType) {
if (msgType == MsgTypeEnum.text ||
(msgType == MsgTypeEnum.robot && item.getAttachment() != null && !((RobotAttachment) item.getAttachment()).isRobotSend())) {
alertDialog.addItem(container.activity.getString(R.string.copy_has_blank), new CustomAlertDialog.onSeparateItemClickListener() {
@Override
public void onClick() {
onCopyMessageItem(item);
}
});
}
}
private void onCopyMessageItem(IMMessage item) {
ClipboardUtil.clipboardCopyText(container.activity, item.getContent());
}
// 长按菜单项 -- 音频转文字
private void longClickItemVoidToText(final IMMessage item, CustomAlertDialog alertDialog, MsgTypeEnum msgType) {
if (msgType != MsgTypeEnum.audio) return;
if (item.getDirect() == MsgDirectionEnum.In
&& item.getAttachStatus() != AttachStatusEnum.transferred)
return;
if (item.getDirect() == MsgDirectionEnum.Out
&& item.getAttachStatus() != AttachStatusEnum.transferred)
return;
alertDialog.addItem(container.activity.getString(R.string.voice_to_text), new CustomAlertDialog.onSeparateItemClickListener() {
@Override
public void onClick() {
onVoiceToText(item);
}
});
}
// 语音转文字
private void onVoiceToText(IMMessage item) {
if (voiceTrans == null)
voiceTrans = new VoiceTrans(container.activity);
voiceTrans.voiceToText(item);
if (item.getDirect() == MsgDirectionEnum.In && item.getStatus() != MsgStatusEnum.read) {
item.setStatus(MsgStatusEnum.read);
NIMClient.getService(MsgService.class).updateIMMessageStatus(item);
adapter.notifyDataSetChanged();
}
}
// 长按菜单项 -- 听筒扬声器切换
private void longClickItemEarPhoneMode(CustomAlertDialog alertDialog, MsgTypeEnum msgType) {
if (msgType != MsgTypeEnum.audio) return;
String content = UserPreferences.isEarPhoneModeEnable() ? ResUtil.getString(R.string.ui_im_messagelistpanelex_05) : ResUtil.getString(R.string.ui_im_messagelistpanelex_06);
final String finalContent = content;
alertDialog.addItem(content, new CustomAlertDialog.onSeparateItemClickListener() {
@Override
public void onClick() {
// Toast.makeText(container.activity, finalContent, Toast.LENGTH_SHORT).show();
SingleToastUtil.showToastShort(finalContent);
setEarPhoneMode(!UserPreferences.isEarPhoneModeEnable(), true);
}
});
}
}
}

View File

@@ -0,0 +1,74 @@
package com.chwl.app.public_chat.ui.message
import android.annotation.SuppressLint
import androidx.lifecycle.MutableLiveData
import com.chwl.app.base.BaseViewModel
import com.chwl.app.public_chat.core.ChatRoomClient
import com.chwl.app.public_chat.core.ChatRoomClientManager
import com.chwl.core.im.custom.bean.CustomAttachment
import com.chwl.core.im.custom.bean.HeadlineChangedAttachment
import com.chwl.core.public_chat_hall.bean.HeadlineBean
import com.chwl.core.public_chat_hall.model.PublicChatModel
import com.netease.nimlib.sdk.chatroom.model.ChatRoomMessage
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum
class PublicChatRoomViewModel : BaseViewModel() {
var chatRoomClient: ChatRoomClient? = null
val headlineLiveData = MutableLiveData<HeadlineBean?>()
fun init(sessionId: String) {
val client = ChatRoomClientManager.getClient(sessionId)
client.messageFilter = ChatRoomClientManager.publicChatRoomReceiveMessageFilter
chatRoomClient = client
registerReceiveMessage(client)
}
@SuppressLint("CheckResult")
fun sendMessage(message: ChatRoomMessage) {
chatRoomClient?.sendMessage(message)?.subscribe({}, {})?.let {
addDisposable(it)
}
}
fun getCurrentHeadline() {
safeLaunch(needLoading = false, onError = {
}) {
// val value = PublicChatModel.getCurrentHeadline()
// updateHeadline(value)
}
}
private fun updateHeadline(headline: HeadlineBean?) {
if (headline?.isValid() == true) {
headlineLiveData.postValue(headline)
} else {
headlineLiveData.postValue(null)
}
}
private fun registerReceiveMessage(chatRoomClient: ChatRoomClient) {
addDisposable(chatRoomClient.messageObservable.subscribe {
it.forEach {
onReceiveMessage(it)
}
})
}
private fun onReceiveMessage(message: ChatRoomMessage) {
if (message.msgType == MsgTypeEnum.custom) {
val attachment: CustomAttachment = (message.attachment as? CustomAttachment) ?: return
when (attachment.first) {
CustomAttachment.CUSTOM_MSG_HEADLINE_CHANGED -> {
when (attachment.second) {
CustomAttachment.CUSTOM_MSG_HEADLINE_CHANGED_SUB -> {
val data = (attachment as? HeadlineChangedAttachment) ?: return
updateHeadline(data.headlineData)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,109 @@
package com.chwl.app.public_chat.ui.message.headline
import android.view.Gravity
import android.view.WindowManager
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.chwl.app.R
import com.chwl.app.base.BaseDialogFragment
import com.chwl.app.common.widget.dialog.DialogManager
import com.chwl.app.databinding.HeadlineSendDialogBinding
import com.chwl.app.public_chat.ui.message.HeadlineViewModel
import com.chwl.app.public_chat.ui.message.PublicChatRoomViewModel
import com.chwl.app.ui.pay.ChargeActivity
import com.chwl.app.ui.widget.dialog.CommonTipDialog
import com.chwl.app.vip.dialog.SelectPayTypeDialog
import com.chwl.core.utils.net.BalanceNotEnoughExeption
import com.chwl.library.utils.ResUtil
import com.chwl.library.utils.SingleToastUtil
import com.example.lib_utils.ktx.singleClick
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class HeadlineSendDialog : BaseDialogFragment<HeadlineSendDialogBinding>() {
private val viewModel by activityViewModels<HeadlineViewModel>()
override var width = WindowManager.LayoutParams.MATCH_PARENT
override var height = WindowManager.LayoutParams.WRAP_CONTENT
override var gravity = Gravity.BOTTOM
private var dialogManager: DialogManager? = null
override fun init() {
dialogManager = DialogManager(context)
lifecycleScope.launch(Dispatchers.Main) {
viewModel.sendHeadlineFlow.collect {
if (it.isSuccess) {
SingleToastUtil.showToast(R.string.sent_success)
dismissAllowingStateLoss()
} else {
if (it.code == BalanceNotEnoughExeption.code) {
showBalanceNotEnoughDialog()
} else {
SingleToastUtil.showToast(it.message)
}
}
}
}
viewModel.loadingLiveData.observe(this) {
if (it) dialogManager?.showProgressDialog(context)
else dialogManager?.dismissDialog()
}
viewModel.headlinePayMoneyLiveData.observe(this) {
binding.tvBuy.text = getString(R.string.headline_buy_format, it?.toString() ?: "-")
}
binding.layoutBuy.singleClick {
val message = getInputContent()
if (message.isEmpty()) {
SingleToastUtil.showToast(R.string.team_view_createteammessageactivity_05)
return@singleClick
}
val money = viewModel.headlinePayMoneyLiveData.value
if (money != null) {
showPayDialog(money)
} else {
SingleToastUtil.showToast(R.string.ui_setting_modifypwdactivity_01)
viewModel.getHeadlinePayMoney()
}
}
viewModel.getHeadlinePayMoney()
}
private fun showPayDialog(money: Long) {
SelectPayTypeDialog.newInstance(
money.toString(),
money,
false
).apply {
setOnDiamondChargeClick {
val message = getInputContent()
viewModel.sendHeadline(message)
}
setOnChargeClick {
ChargeActivity.start(context)
}
}.show(context)
}
private fun getInputContent(): String {
return binding.etContent.text?.trim()?.toString() ?: ""
}
private fun showBalanceNotEnoughDialog() {
val tipDialog = CommonTipDialog(context)
tipDialog.setTipMsg(ResUtil.getString(R.string.insufficient_balance_recharge_tips))
tipDialog.setOkText(getString(R.string.charge))
tipDialog.setOnActionListener(
object : CommonTipDialog.OnActionListener {
override fun onOk() {
ChargeActivity.start(context)
}
}
)
tipDialog.show()
}
override fun onDestroy() {
super.onDestroy()
dialogManager?.dismissDialog()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_white_top_18dp">
<EditText
android:id="@+id/et_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="@dimen/dp_15"
android:layout_marginTop="@dimen/dp_19"
android:background="@drawable/shape_f5f5f5_16dp"
android:hint="@string/headline_input_hint"
android:maxLength="100"
android:paddingHorizontal="@dimen/dp_5"
android:paddingVertical="@dimen/dp_11"
android:textColor="@color/color_333333"
android:textSize="@dimen/dp_13"
app:layout_constraintDimensionRatio="344:132"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/layout_buy"
android:layout_width="wrap_content"
android:layout_height="@dimen/dp_39"
android:layout_marginTop="@dimen/dp_26"
android:layout_marginBottom="@dimen/dp_27"
android:background="@drawable/base_shape_theme_20dp"
android:gravity="center"
android:minWidth="@dimen/dp_200"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_content">
<ImageView
android:layout_width="@dimen/dp_18"
android:layout_height="@dimen/dp_18"
android:src="@drawable/ic_diamond" />
<TextView
android:id="@+id/tv_buy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="@dimen/dp_16" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.chwl.app.base.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_headline_add"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="@dimen/dp_2"
android:background="@drawable/public_chat_bg_headline_empty"
app:layout_constraintDimensionRatio="371:101.5"
app:layout_constraintTop_toBottomOf="@id/title_bar">
<ImageView
android:id="@+id/iv_headline_empty"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="@dimen/dp_3"
android:src="@drawable/public_chat_ic_headline_empty_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@id/tv_headline_empty"
app:layout_constraintHeight_percent="0.128"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.587" />
<TextView
android:id="@+id/tv_headline_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/want_headline"
android:textColor="#F9DB3B"
android:textSize="@dimen/dp_11"
app:layout_constraintBottom_toBottomOf="@id/iv_headline_empty"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_headline_empty"
app:layout_constraintTop_toTopOf="@id/iv_headline_empty" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_headline_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="iv_headline_empty,tv_headline_empty" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/dp_1_5"
android:background="@drawable/public_chat_bg_headline"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/title_bar"
tools:visibility="visible">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_headline_user_avatar"
android:layout_width="@dimen/dp_25"
android:layout_height="@dimen/dp_25"
android:layout_marginStart="@dimen/dp_11"
android:layout_marginTop="@dimen/dp_17"
android:scaleType="centerCrop"
android:src="@drawable/default_avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/shape_circle" />
<ImageView
android:layout_width="@dimen/dp_28"
android:layout_height="@dimen/dp_28"
android:src="@drawable/public_chat_bg_headline_avatar_frame"
app:layout_constraintBottom_toBottomOf="@id/iv_headline_user_avatar"
app:layout_constraintEnd_toEndOf="@id/iv_headline_user_avatar"
app:layout_constraintStart_toStartOf="@id/iv_headline_user_avatar"
app:layout_constraintTop_toTopOf="@id/iv_headline_user_avatar" />
<TextView
android:id="@+id/tv_headline_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_5"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#F5EA8E"
android:textSize="@dimen/dp_14"
app:layout_constraintBottom_toBottomOf="@id/iv_headline_user_avatar"
app:layout_constraintStart_toEndOf="@id/iv_headline_user_avatar"
app:layout_constraintTop_toTopOf="@id/iv_headline_user_avatar"
tools:text="Name" />
<com.chwl.app.view.AutoMirroredImageView
android:id="@+id/iv_money_bg"
android:layout_width="@dimen/dp_40"
android:layout_height="@dimen/dp_16"
android:src="@drawable/public_chat_bg_headline_money"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_headline_money"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_6"
android:maxLines="1"
android:text="0"
android:textColor="@color/white"
android:textSize="@dimen/dp_8"
app:layout_constraintBottom_toBottomOf="@id/iv_money_bg"
app:layout_constraintEnd_toEndOf="@id/iv_money_bg"
app:layout_constraintTop_toTopOf="@id/iv_money_bg" />
<ImageView
android:layout_width="@dimen/dp_10"
android:layout_height="@dimen/dp_10"
android:layout_marginEnd="@dimen/dp_2"
android:scaleType="fitCenter"
android:src="@drawable/icon_diamond"
app:layout_constraintBottom_toBottomOf="@id/iv_money_bg"
app:layout_constraintEnd_toStartOf="@id/tv_headline_money"
app:layout_constraintTop_toTopOf="@id/iv_money_bg" />
<TextView
android:id="@+id/tv_headline_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/dp_13"
android:layout_marginTop="@dimen/dp_4"
android:layout_marginBottom="@dimen/dp_12"
android:textColor="#FFFADF"
android:textSize="@dimen/dp_14"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_headline_user_avatar"
tools:text="Content" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_headline_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="layout_headline,layout_headline_add" />
<FrameLayout
android:id="@+id/fragment_message"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_headline_bottom" />
<ImageView
android:id="@+id/iv_headline"
android:layout_width="@dimen/dp_96"
android:layout_height="@dimen/dp_80"
android:src="@drawable/public_chat_ic_want_headline"
app:layout_constraintBottom_toBottomOf="@id/fragment_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/fragment_message" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/messageActivityLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/message_activity_list_view_container"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1">
<com.netease.nim.uikit.business.session.helper.MsgBkImageView
android:id="@+id/message_activity_background"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/team_notify_bar_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messageListView"
style="@style/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<FrameLayout
android:id="@+id/layoutPlayAudio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/nim_dialog_toast_bg"
android:visibility="gone">
<Chronometer
android:id="@+id/timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/timer_default"
android:textColor="@color/white"
android:textSize="45sp" />
<LinearLayout
android:id="@+id/timer_tip_container"
android:layout_width="188dp"
android:layout_height="40dp"
android:layout_gravity="bottom"
android:gravity="center">
<TextView
android:id="@+id/timer_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/recording_cancel"
android:textColor="@color/white"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>
<ImageView
android:id="@+id/iv_audio_party"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="10dp"
android:background="@drawable/icon_party2"
android:text="@string/layout_nim_message_fragment_01"
android:visibility="gone" />
</FrameLayout>
<include layout="@layout/nim_chat_room_message_activity_bottom_layout" />
</LinearLayout>
</layout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dip"
android:layout_marginEnd="7dip"
android:gravity="center"
android:paddingStart="7dip"
android:paddingEnd="7dip"
android:textColor="@color/color_black_333333"
android:textSize="9sp" />
</merge>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="public_chat_room">غرفة الدردشة العامة</string>
<string name="public_chat">دردشة عامة</string>
<string name="want_headline">اريد تصدر العناوين </string>
<string name="public_chat_not_found">فشل في الحصول على معلومات غرفة الدردشة العامة</string>
<string name="headline_buy_format">تصدر العناوين %s</string>
<string name="headline_message_format">~عزيزي%s، هيا تصدر العناوين </string>
<string name="insufficient_balance_recharge_tips">رصيد العملات الذهبية غير كافي ،هل تريد إعادة الشحن علي الفور؟</string>
<string name="headline_input_hint">الرجاء إدخال المحتوي الذي تريد أن تتصدر عناوينهّ~ (يقتصر على 100 كلمة)</string>
<string name="headline_input_length_limit_tips">المحتوي الذي تريد أن تتصدر عناوينهّ يقتصر على 100 كلمة</string>
<string name="sent_success">أرسل بنجاح</string>
</resources>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="public_chat_room">公聊廳</string>
<string name="public_chat">公聊</string>
<string name="want_headline">我要上頭條</string>
<string name="public_chat_not_found">獲取公聊廳信息失敗</string>
<string name="headline_buy_format">%s上頭條</string>
<string name="headline_message_format">尊貴的%s上頭條啦~</string>
<string name="insufficient_balance_recharge_tips">金幣餘額不足,是否立即儲值?</string>
<string name="headline_input_hint">請輸入想上頭條的內容呀~僅限100字</string>
<string name="headline_input_length_limit_tips">頭條字數請勿超過100字~</string>
<string name="permission_mic_tips">請給予麥克風權限後再試</string>
<string name="sent_success">發送成功</string>
</resources>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="public_chat_room">Chatting</string>
<string name="public_chat">Chatting</string>
<string name="want_headline">Broadcast</string>
<string name="public_chat_not_found">Failed to retrieve information</string>
<string name="headline_buy_format">%s Broadcast</string>
<string name="headline_message_format">Honorable %s sent a broadcast~</string>
<string name="insufficient_balance_recharge_tips">The coin is insufficientWould you like to recharge?</string>
<string name="headline_input_hint">Please enter the content you want to broadcast~(100 characters)</string>
<string name="headline_input_length_limit_tips">Please do not exceed 100 characters for the broadcast~</string>
<string name="sent_success">Sent successfully</string>
</resources>