import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';
import { ipcRenderer, remote } from 'electron';
import clazz from 'classname';
import moment from 'moment';
import axios from 'axios';
import classes from './style.css';
import Avatar from 'components/Avatar';
import helper from 'utils/helper';
import { parser as emojiParse } from 'utils/emoji';
import { on, off } from 'utils/event';
@inject(stores => ({
user: stores.chat.user,
sticky: stores.chat.sticky,
empty: stores.chat.empty,
removeChat: stores.chat.removeChat,
messages: stores.chat.messages,
loading: stores.session.loading,
reset: () => {
stores.chat.user = false;
},
isFriend: (id) => {
var user = stores.contacts.memberList.find(e => e.UserName === id) || {};
return helper.isContact(user);
},
showUserinfo: async(isme, user) => {
var caniremove = helper.isChatRoomOwner(stores.chat.user);
if (isme) {
user = stores.session.user.User;
} else {
stores.contacts.memberList.find(e => {
// Try to find contact in your contacts
if (e.UserName === user.UserName) {
return (user = e);
}
});
}
stores.userinfo.toggle(true, user, caniremove);
},
getMessage: (messageid) => {
var list = stores.chat.messages.get(stores.chat.user.UserName);
return list.data.find(e => e.MsgId === messageid);
},
deleteMessage: (messageid) => {
stores.chat.deleteMessage(stores.chat.user.UserName, messageid);
},
showMembers: (user) => {
if (helper.isChatRoom(user.UserName)) {
stores.members.toggle(true, user);
}
},
showContact: (userid) => {
var user = stores.contacts.memberList.find(e => e.UserName === userid);
stores.userinfo.toggle(true, user);
},
showForward: (message) => stores.forward.toggle(true, message),
parseMessage: (message, from) => {
var isChatRoom = message.isme ? false : helper.isChatRoom(message.FromUserName);
var user = from;
message = Object.assign({}, message);
if (isChatRoom) {
let matchs = message.Content.split(':
');
// Get the newest chat room infomation
from = stores.contacts.memberList.find(e => from.UserName === e.UserName);
user = from.MemberList.find(e => e.UserName === matchs[0]);
message.Content = matchs[1];
}
// If user is null, that mean user has been removed from this chat room
return { message, user };
},
showAddFriend: (user) => stores.addfriend.toggle(true, user),
recallMessage: stores.chat.recallMessage,
downloads: stores.settings.downloads,
rememberConversation: stores.settings.rememberConversation,
showConversation: stores.chat.showConversation,
toggleConversation: stores.chat.toggleConversation,
}))
@observer
export default class ChatContent extends Component {
getMessageContent(message) {
var uploading = message.uploading;
switch (message.MsgType) {
case 1:
if (message.location) {
return `
`;
}
// Text message
return emojiParse(message.Content);
case 3:
// Image
let image = message.image;
if (uploading) {
return `
`;
}
return `
`;
case 34:
/* eslint-disable */
// Voice
let voice = message.voice;
let times = message.VoiceLength;
let width = 40 + 7 * (times / 2000);
let seconds = 0;
/* eslint-enable */
if (times < 60 * 1000) {
seconds = Math.ceil(times / 1000);
}
return `
`;
case 47:
case 49 + 8:
// External emoji
let emoji = message.emoji;
if (emoji) {
if (uploading) {
return `
`;
}
return `
`;
}
return `
`;
case 42:
// Contact Card
let contact = message.contact;
let isFriend = this.props.isFriend(contact.UserName);
let html = `
';
return html;
case 43:
// Video message
let video = message.video;
if (uploading) {
return `
`;
}
if (!video) {
console.error('Invalid video message: %o', message);
return `
收到无法解析的视频消息,请参阅控制台输出
`;
}
return `
`;
case 49 + 2000:
// Money transfer
let transfer = message.transfer;
return `
转账消息
💰 ${transfer.money}
如需收钱,请打开手机微信确认收款。
`;
case 49 + 6:
// File message
let file = message.file;
let download = message.download;
/* eslint-disable */
return `
${file.name}
${helper.humanSize(file.size)}
${
uploading
? '
'
: (download.done ? '
' : '
')
}
`;
/* eslint-enable */
case 49 + 17:
// Location sharing...
return `
位置分享,请查看其他客户端
`;
}
}
renderMessages(list, from) {
return list.data.map((e, index) => {
var { message, user } = this.props.parseMessage(e, from);
var type = message.MsgType;
if ([
// WeChat system message
10000,
// Custome message
19999
].includes(type)) {
return (
);
}
if (!user) {
return false;
}
return (
this.props.showUserinfo(message.isme, user)}
/>
this.showMessageAction(message)}
dangerouslySetInnerHTML={{__html: this.getMessageContent(message)}} />
{ moment(message.CreateTime * 1000).fromNow() }
);
});
}
async handleClick(e) {
var target = e.target;
// Open the image
if (target.tagName === 'IMG'
&& target.classList.contains('open-image')) {
// Get image from cache and convert to base64
let response = await axios.get(target.src, { responseType: 'arraybuffer' });
// eslint-disable-next-line
let base64 = new window.Buffer(response.data, 'binary').toString('base64');
ipcRenderer.send('open-image', {
dataset: target.dataset,
base64,
});
return;
}
// Play the voice message
if (target.tagName === 'DIV'
&& target.classList.contains('play-voice')) {
let audio = target.querySelector('audio');
audio.onplay = () => target.classList.add(classes.playing);
audio.onended = () => target.classList.remove(classes.playing);
audio.play();
return;
}
// Open the location
if (target.tagName === 'IMG'
&& target.classList.contains('open-map')) {
ipcRenderer.send('open-map', {
map: target.dataset.map,
});
}
// Show contact card
if (target.tagName === 'DIV'
&& target.classList.contains('is-friend')) {
this.props.showContact(target.dataset.userid);
}
// Add new friend
if (target.tagName === 'I'
&& target.classList.contains('icon-ion-android-add')) {
this.props.showAddFriend({
UserName: target.dataset.userid
});
}
// Add new friend
if (target.tagName === 'A'
&& target.classList.contains('add-friend')) {
this.props.showAddFriend({
UserName: target.dataset.userid
});
}
// Open file & open folder
if (target.tagName === 'I'
&& target.classList.contains('is-file')) {
let message = this.props.getMessage(e.target.parentElement.dataset.id);
this.showFileAction(message.download);
}
// Download file
if (target.tagName === 'I'
&& target.classList.contains('is-download')) {
let message = this.props.getMessage(e.target.parentElement.dataset.id);
let response = await axios.get(message.file.download, { responseType: 'arraybuffer' });
// eslint-disable-next-line
let base64 = new window.Buffer(response.data, 'binary').toString('base64');
let filename = ipcRenderer.sendSync(
'file-download',
{
filename: `${this.props.downloads}/${message.MsgId}_${message.file.name}`,
raw: base64,
},
);
setTimeout(() => {
message.download = {
done: true,
path: filename,
};
});
}
}
showFileAction(download) {
var templates = [
{
label: 'Open file',
click: () => {
ipcRenderer.send('open-file', download.path);
}
},
{
label: 'Open the folder',
click: () => {
let dir = download.path.split('/').slice(0, -1).join('/');
ipcRenderer.send('open-folder', dir);
}
},
];
var menu = new remote.Menu.buildFromTemplate(templates);
menu.popup(remote.getCurrentWindow());
}
showMessageAction(message) {
var caniforward = [1, 3, 47, 43, 49 + 6].includes(message.MsgType);
var templates = [
{
label: '删除',
click: () => {
this.props.deleteMessage(message.MsgId);
}
},
];
var menu;
if (caniforward) {
templates.unshift({
label: '转发',
click: () => {
this.props.showForward(message);
}
});
}
if (message.isme
&& message.CreateTime - new Date() < 2 * 60 * 1000) {
templates.unshift({
label: '撤回',
click: () => {
this.props.recallMessage(message);
}
});
}
if (message.uploading) return;
menu = new remote.Menu.buildFromTemplate(templates);
menu.popup(remote.getCurrentWindow());
}
showMenu() {
var user = this.props.user;
var menu = new remote.Menu.buildFromTemplate([
{
label: '切换对话',
click: () => {
this.props.toggleConversation();
}
},
{
type: 'separator',
},
{
label: '清空聊天内容',
click: () => {
this.props.empty(user);
}
},
{
type: 'separator'
},
{
label: helper.isTop(user) ? '取消置顶' : '聊天置顶',
click: () => {
this.props.sticky(user);
}
},
{
label: '删除',
click: () => {
this.props.removeChat(user);
}
},
]);
menu.popup(remote.getCurrentWindow());
}
handleScroll(e) {
var tips = this.refs.tips;
var viewport = e.target;
var unread = viewport.querySelectorAll(`.${classes.message}.unread`);
var rect = viewport.getBoundingClientRect();
var counter = 0;
Array.from(unread).map(e => {
if (e.getBoundingClientRect().top > rect.bottom) {
counter += 1;
} else {
e.classList.remove('unread');
}
});
if (counter) {
tips.innerHTML = `你有${counter}个未读消息`;
tips.classList.add(classes.show);
} else {
tips.classList.remove(classes.show);
}
}
scrollBottomWhenSentMessage() {
var { user, messages } = this.props;
var list = messages.get(user.id);
return list.slice(-1).isme;
}
componentWillUnmount() {
!this.props.rememberConversation && this.props.reset();
}
componentDidUpdate() {
var viewport = this.refs.viewport;
var tips = this.refs.tips;
if (viewport) {
let newestMessage = this.props.messages.get(this.props.user.UserName).data.slice(-1)[0];
let images = viewport.querySelectorAll('img.unload');
// Scroll to bottom when you sent message
if (newestMessage
&& newestMessage.isme) {
viewport.scrollTop = viewport.scrollHeight;
return;
}
// Show the unread messages count
if (viewport.scrollTop < this.scrollTop) {
let counter = viewport.querySelectorAll(`.${classes.message}.unread`).length;
if (counter) {
tips.innerHTML = `你有${counter}个未读消息`;
tips.classList.add(classes.show);
}
return;
}
// Auto scroll to bottom when message has been loaded
Array.from(images).map(e => {
on(e, 'load', ev => {
off(e, 'load');
e.classList.remove('unload');
viewport.scrollTop = viewport.scrollHeight;
this.scrollTop = viewport.scrollTop;
});
on(e, 'error', ev => {
var fallback = ev.target.dataset.fallback;
if (fallback === 'undefined') {
fallback = 'assets/images/broken.png';
}
ev.target.src = fallback;
ev.target.removeAttribute('data-fallback');
off(e, 'error');
});
});
// Hide the unread message count
tips.classList.remove(classes.show);
viewport.scrollTop = viewport.scrollHeight;
this.scrollTop = viewport.scrollTop;
// Mark message has been loaded
Array.from(viewport.querySelectorAll(`.${classes.message}.unread`)).map(e => e.classList.remove('unread'));
}
}
componentWillReceiveProps(nextProps) {
// When the chat user has been changed, show the last message in viewport
if (this.props.user && nextProps.user
&& this.props.user.UserName !== nextProps.user.UserName) {
this.scrollTop = -1;
}
}
render() {
var { loading, showConversation, user, messages } = this.props;
var title = user.RemarkName || user.NickName;
var signature = user.Signature;
if (loading) return false;
return (
this.handleClick(e)}>
{
user ? (
this.props.showMembers(user)}
title={signature} />
this.showMenu()} />
this.handleScroll(e)}
ref="viewport">
{
this.renderMessages(messages.get(user.UserName), user)
}
) : (
未选择聊天 :(
)
}
Unread message.
);
}
}