wewechat++ init
仓库提交至星火社区作品集 Signed-off-by: Riceneeder <86492950+Riceneeder@users.noreply.github.com>
This commit is contained in:
38
src/js/components/Avatar/index.js
Normal file
38
src/js/components/Avatar/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './style.global.css';
|
||||
|
||||
export default class Avatar extends Component {
|
||||
static propTypes = {
|
||||
src: PropTypes.string,
|
||||
fallback: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fallback: 'assets/images/user-fallback.png',
|
||||
};
|
||||
|
||||
handleError(e) {
|
||||
e.target.src = this.props.fallback;
|
||||
}
|
||||
|
||||
handleLoad(e) {
|
||||
e.target.classList.remove('fadein');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.src) return false;
|
||||
|
||||
return (
|
||||
<img
|
||||
className={`Avatar fade fadein ${this.props.className}`}
|
||||
onClick={this.props.onClick}
|
||||
onLoad={e => this.handleLoad(e)}
|
||||
onError={e => this.handleError(e)}
|
||||
src={this.props.src}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
6
src/js/components/Avatar/style.global.css
Normal file
6
src/js/components/Avatar/style.global.css
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
.Avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
53
src/js/components/Loader/index.js
Normal file
53
src/js/components/Loader/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Transition from 'react-addons-css-transition-group';
|
||||
import clazz from 'classname';
|
||||
|
||||
import './style.global.css';
|
||||
|
||||
export default class Button extends Component {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool,
|
||||
fullscreen: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
show: false,
|
||||
fullscreen: false,
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
if (!this.props.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clazz('Loader', this.props.className, {
|
||||
'Loader--fullscreen': this.props.fullscreen
|
||||
})}>
|
||||
<svg className="Loader-circular">
|
||||
<circle
|
||||
className="Loader-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
fill="none"
|
||||
r="20"
|
||||
strokeWidth="5"
|
||||
strokeMiterlimit="10" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Transition
|
||||
transitionName="Loader"
|
||||
transitionEnterTimeout={200}
|
||||
transitionLeaveTimeout={200}>
|
||||
{this.renderContent()}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
}
|
||||
82
src/js/components/Loader/style.global.css
Normal file
82
src/js/components/Loader/style.global.css
Normal file
@@ -0,0 +1,82 @@
|
||||
|
||||
.Loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
z-index: 999;
|
||||
|
||||
&.Loader--fullscreen {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
.Loader-enter {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: .2s;
|
||||
|
||||
&.Loader-enter-active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.Loader-leave {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: .2s;
|
||||
|
||||
&.Loader-leave-active {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.Loader-circular {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: block;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
margin-top: -50px;
|
||||
margin-left: -50px;
|
||||
animation: Loader-rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.Loader-path {
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
stroke: #039be5;
|
||||
stroke-width: 3;
|
||||
animation: Loader-dash 1.5s ease-in-out infinite;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
@keyframes Loader-rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes Loader-dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 89, 200;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 89, 200;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
||||
|
||||
84
src/js/components/MessageInput/Emoji/index.js
Normal file
84
src/js/components/MessageInput/Emoji/index.js
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import clazz from 'classname';
|
||||
import delegate from 'delegate';
|
||||
|
||||
import classes from './style.css';
|
||||
import { emoji } from 'utils/emoji';
|
||||
|
||||
export default class Emoji extends Component {
|
||||
static propTypes = {
|
||||
output: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
delegate(this.refs.container, 'a.qqemoji', 'click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.output(e.target.title);
|
||||
this.props.close();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.show) {
|
||||
this.refs.container.focus();
|
||||
}
|
||||
}
|
||||
|
||||
renderEmoji(emoji) {
|
||||
return emoji.map((e, index) => {
|
||||
var { key, className } = e;
|
||||
return (
|
||||
<a
|
||||
className={className}
|
||||
key={index}
|
||||
title={key} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
ref="container"
|
||||
tabIndex="-1"
|
||||
className={clazz(classes.container, {
|
||||
[classes.show]: this.props.show
|
||||
})}
|
||||
onBlur={e => this.props.close()}>
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(0, 15))}
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(15, 30))}
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(30, 45))}
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(45, 60))}
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(60, 75))}
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(75, 90))}
|
||||
</div>
|
||||
|
||||
<div className={classes.row}>
|
||||
{this.renderEmoji(emoji.slice(90, 105))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/js/components/MessageInput/Emoji/style.css
Normal file
47
src/js/components/MessageInput/Emoji/style.css
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 0;
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 6px 28px 0 rgba(230, 230, 230, 1);
|
||||
z-index: 99;
|
||||
outline: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
& a {
|
||||
margin: 4px;
|
||||
cursor: pointer;
|
||||
zoom: 1.1;
|
||||
transition: .2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
bottom: 46px;
|
||||
padding: 6px 10px;
|
||||
|
||||
& a {
|
||||
margin: 3px;
|
||||
zoom: .9;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/js/components/MessageInput/Suggestion/index.js
Normal file
56
src/js/components/MessageInput/Suggestion/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import clazz from 'classname';
|
||||
|
||||
import './style.global.css';
|
||||
import TransitionPortal from 'components/TransitionPortal';
|
||||
|
||||
export default class Suggestion extends Component {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
list: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
var { show, list, selected } = this.props;
|
||||
|
||||
if (!show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(window.event);
|
||||
|
||||
return (
|
||||
<div className="Suggestion">
|
||||
{
|
||||
list.map((e, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={clazz('Suggestion-item', {
|
||||
'Suggestion--selected': e.UserName === selected
|
||||
})}>
|
||||
<img src={e.HeadImgUrl} />
|
||||
|
||||
<div className="Suggestion-user">
|
||||
<p className="Suggestion-username" dangerouslySetInnerHTML={{__html: e.RemarkName || e.NickName}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TransitionPortal transitionEnterTimeout={0} transitionLeaveTimeout={150} transitionName="Suggestion">
|
||||
{
|
||||
this.renderContent()
|
||||
}
|
||||
</TransitionPortal>
|
||||
);
|
||||
};
|
||||
}
|
||||
120
src/js/components/MessageInput/Suggestion/style.css
Normal file
120
src/js/components/MessageInput/Suggestion/style.css
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
.Suggestion {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 311px;
|
||||
height: calc(100vh - 200px);
|
||||
background: #fff;
|
||||
box-shadow: 0 0 24px 0 rgba(0, 0, 0, .2);
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.Suggestion-item {
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Suggestion--selected,
|
||||
.Suggestion-item:hover {
|
||||
background: #405de6;
|
||||
}
|
||||
|
||||
.Suggestion--selected .Suggestion-username,
|
||||
.Suggestion-item:hover .Suggestion-username {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.Suggestion-item img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.Suggestion-username {
|
||||
color: #777;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.Suggestion-enter {
|
||||
transform: translateY(24px);
|
||||
opacity: 0;
|
||||
transition: .2s cubic-bezier(.5, -.55, .4, 1.55);
|
||||
}
|
||||
|
||||
.Suggestion-enter.Suggestion-enter-active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.Suggestion-leave {
|
||||
opacity: 1;
|
||||
transition: .14s;
|
||||
}
|
||||
|
||||
.Suggestion-leave.Suggestion-leave-active {
|
||||
transform: translateY(-24px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.Suggestion-input-user {
|
||||
display: flex;
|
||||
padding: 0 23px;
|
||||
margin: 0 2px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
background: rgba(230, 230, 230, 1);
|
||||
color: #777;
|
||||
border-radius: 32px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Suggestion-input-user img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.Suggestion-input-user * {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.Suggestion {
|
||||
bottom: 48px;
|
||||
left: 280px;
|
||||
}
|
||||
|
||||
.Suggestion-item {
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.Suggestion-item img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.Suggestion-username {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.Suggestion-input-user {
|
||||
padding: 0 23px;
|
||||
margin: 0 2px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.Suggestion-input-user img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
226
src/js/components/MessageInput/index.js
Normal file
226
src/js/components/MessageInput/index.js
Normal file
@@ -0,0 +1,226 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import clazz from 'classname';
|
||||
|
||||
import classes from './style.css';
|
||||
import Emoji from './Emoji';
|
||||
|
||||
export default class MessageInput extends Component {
|
||||
static propTypes = {
|
||||
me: PropTypes.object,
|
||||
sendMessage: PropTypes.func.isRequired,
|
||||
showMessage: PropTypes.func.isRequired,
|
||||
user: PropTypes.array.isRequired,
|
||||
confirmSendImage: PropTypes.func.isRequired,
|
||||
process: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
me: {},
|
||||
};
|
||||
|
||||
canisend() {
|
||||
var user = this.props.user;
|
||||
|
||||
if (
|
||||
true
|
||||
&& user.length === 1
|
||||
&& user.slice(-1).pop().UserName === this.props.me.UserName
|
||||
) {
|
||||
this.props.showMessage('Can\'t send messages to yourself.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleEnter(e) {
|
||||
var message = this.refs.input.value.trim();
|
||||
var user = this.props.user;
|
||||
var batch = user.length > 1;
|
||||
|
||||
if (
|
||||
false
|
||||
|| !this.canisend()
|
||||
|| !message
|
||||
|| e.charCode !== 13
|
||||
) return;
|
||||
|
||||
// You can not send message to yourself
|
||||
Promise.all(
|
||||
user.filter(e => e.UserName !== this.props.me.UserName).map(
|
||||
async e => {
|
||||
let res = await this.props.sendMessage(
|
||||
e,
|
||||
{
|
||||
content: message,
|
||||
type: 1,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if (!res) {
|
||||
await this.props.showMessage(batch ? `Sending message to ${e.NickName} has failed!` : 'Failed to send message.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.refs.input.value = '';
|
||||
}
|
||||
|
||||
state = {
|
||||
showEmoji: false
|
||||
};
|
||||
|
||||
toggleEmoji(show = !this.state.showEmoji) {
|
||||
this.setState({ showEmoji: show });
|
||||
}
|
||||
|
||||
writeEmoji(emoji) {
|
||||
var input = this.refs.input;
|
||||
|
||||
input.value += `[${emoji}]`;
|
||||
input.focus();
|
||||
}
|
||||
|
||||
async batchProcess(file) {
|
||||
var message;
|
||||
var batch = this.props.user.length > 1;
|
||||
var receiver = this.props.user.filter(e => e.UserName !== this.props.me.UserName);
|
||||
var showMessage = this.props.showMessage;
|
||||
|
||||
if (this.canisend() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let user of receiver) {
|
||||
if (message) {
|
||||
await this.props.sendMessage(user, message, true)
|
||||
.catch(ex => showMessage(`Sending message to ${user.NickName} has failed!`));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not repeat upload file, forward the message to another user
|
||||
message = await this.props.process(file, user);
|
||||
|
||||
if (message === false) {
|
||||
if (batch) {
|
||||
showMessage(`Send message to ${user.NickName} is failed!`);
|
||||
continue;
|
||||
}
|
||||
// In batch mode just show the failed message
|
||||
showMessage('Failed to send image.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handlePaste(e) {
|
||||
var args = ipcRenderer.sendSync('file-paste');
|
||||
|
||||
if (args.hasImage && this.canisend()) {
|
||||
e.preventDefault();
|
||||
|
||||
if ((await this.props.confirmSendImage(args.filename)) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parts = [
|
||||
new window.Blob([new window.Uint8Array(args.raw.data)], { type: 'image/png' })
|
||||
];
|
||||
let file = new window.File(parts, args.filename, {
|
||||
lastModified: new Date(),
|
||||
type: 'image/png'
|
||||
});
|
||||
|
||||
this.batchProcess(file);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
var input = this.refs.input;
|
||||
|
||||
// When user has changed clear the input
|
||||
if (
|
||||
true
|
||||
&& input
|
||||
&& input.value
|
||||
&& this.props.user.map(e => e.UserName).join() !== nextProps.user.map(e => e.UserName).join()
|
||||
) {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var canisend = !!this.props.user.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
clazz(
|
||||
classes.container,
|
||||
this.props.className,
|
||||
{
|
||||
[classes.shouldSelectUser]: !canisend,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classes.tips}
|
||||
>
|
||||
You should choose a contact first.
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="messageInput"
|
||||
ref="input"
|
||||
type="text"
|
||||
placeholder="Type something to send..."
|
||||
readOnly={!canisend}
|
||||
onPaste={e => this.handlePaste(e)}
|
||||
onKeyPress={e => this.handleEnter(e)}
|
||||
/>
|
||||
|
||||
<div className={classes.action}>
|
||||
<i
|
||||
className="icon-ion-android-attach"
|
||||
id="showUploader"
|
||||
onClick={e => canisend && this.refs.uploader.click()}
|
||||
/>
|
||||
|
||||
<i
|
||||
className="icon-ion-ios-heart"
|
||||
id="showEmoji"
|
||||
onClick={e => canisend && this.toggleEmoji(true)}
|
||||
style={{
|
||||
color: 'red',
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
onChange={e => {
|
||||
this.batchProcess(e.target.files[0]);
|
||||
e.target.value = '';
|
||||
}}
|
||||
ref="uploader"
|
||||
style={{
|
||||
display: 'none',
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
<Emoji
|
||||
close={e => setTimeout(() => this.toggleEmoji(false), 100)}
|
||||
output={emoji => this.writeEmoji(emoji)}
|
||||
show={this.state.showEmoji}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
94
src/js/components/MessageInput/style.css
Normal file
94
src/js/components/MessageInput/style.css
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& input {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
margin-left: 32px;
|
||||
line-height: 60px;
|
||||
border: 0;
|
||||
padding-right: 17px;
|
||||
background: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
& i {
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& i:hover {
|
||||
color: #34b7f1;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 65px;
|
||||
margin-right: 17px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tips {
|
||||
position: absolute;
|
||||
height: 32px;
|
||||
left: 14px;
|
||||
top: -32px;
|
||||
padding: 0 16px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background: color(#616161 -alpha(10%));
|
||||
border-radius: 1px;
|
||||
opacity: 0;
|
||||
transform-origin: center bottom;
|
||||
transition: .15s cubic-bezier(0, 0, .2, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shouldSelectUser:hover .tips {
|
||||
opacity: 1;
|
||||
top: calc(-32px - 24px);
|
||||
}
|
||||
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
height: 46px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
margin-left: 20px;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
& i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& .action {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
height: 24px;
|
||||
top: -24px;
|
||||
padding: 0 12px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.shouldSelectUser:hover .tips {
|
||||
opacity: 1;
|
||||
top: calc(-24px - 12px);
|
||||
}
|
||||
}
|
||||
130
src/js/components/Modal/index.js
Normal file
130
src/js/components/Modal/index.js
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Transition from 'react-addons-css-transition-group';
|
||||
import clazz from 'classname';
|
||||
|
||||
import './style.css';
|
||||
import TransitionPortal from '../TransitionPortal';
|
||||
import { on, off } from 'utils/event';
|
||||
|
||||
class ModalBody extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Transition
|
||||
transitionName="fade"
|
||||
transitionEnterTimeout={1000}
|
||||
transitionLeaveTimeout={1000}>
|
||||
<div
|
||||
className={clazz('Modal-body', this.props.className)}
|
||||
style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
class ModalHeader extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={clazz('Modal-header', this.props.className)}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ModalFooter extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={clazz('Modal-footer', this.props.className)}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Modal extends Component {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
overlay: PropTypes.bool,
|
||||
onCancel: PropTypes.func,
|
||||
transition4overlay: PropTypes.string,
|
||||
transition4body: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
overlay: true,
|
||||
transition4overlay: 'Modal-overlay',
|
||||
transition4body: 'Modal-body',
|
||||
onCancel: Function,
|
||||
};
|
||||
|
||||
renderOverlay() {
|
||||
if (!this.props.show || !this.props.overlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clazz('Modal-overlay', this.props.className)}
|
||||
onClick={this.props.onCancel} />
|
||||
);
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
if (!this.props.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clazz('Modal-content', this.props.className)}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleEscKey(e) {
|
||||
if (e.keyCode === 27 && this.props.show) {
|
||||
this.props.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
off(document, 'keydown', this.handleEscKey);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleEscKey = this.handleEscKey.bind(this);
|
||||
on(document, 'keydown', this.handleEscKey);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!/MSIE\s8\.0/.test(window.navigator.userAgent)) {
|
||||
document.body.style.overflow = this.props.show ? 'hidden' : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Modal" ref="node">
|
||||
<Transition
|
||||
transitionName={this.props.transition4overlay}
|
||||
transitionEnterTimeout={200}
|
||||
transitionLeaveTimeout={200}
|
||||
ref="overlay">
|
||||
{this.renderOverlay()}
|
||||
</Transition>
|
||||
|
||||
<TransitionPortal
|
||||
transitionName={this.props.transition4body}
|
||||
transitionEnterTimeout={200}
|
||||
transitionLeaveTimeout={140}
|
||||
ref="content">
|
||||
{this.renderBody()}
|
||||
</TransitionPortal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { Modal, ModalBody, ModalHeader, ModalFooter };
|
||||
99
src/js/components/Modal/style.css
Normal file
99
src/js/components/Modal/style.css
Normal file
@@ -0,0 +1,99 @@
|
||||
|
||||
:global .Modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
:global .Modal-header {
|
||||
position: relative;
|
||||
padding: 8px 20px 4px;
|
||||
color: #777;
|
||||
border-bottom: thin solid #d9d9d9;
|
||||
|
||||
& .Modal-close {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
color: $google;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global .Modal-footer {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
:global .Modal-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
min-width: 240px;
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
z-index: 999;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
& > div {
|
||||
pointer-events: all;
|
||||
background: #fff;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
|
||||
}
|
||||
}
|
||||
|
||||
:global .Modal-body {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
:global .Modal-overlay-enter {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: .2s;
|
||||
|
||||
&.Modal-overlay-enter-active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
:global .Modal-overlay-leave {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: .2s;
|
||||
|
||||
&.Modal-overlay-leave-active {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
:global .Modal-body-enter {
|
||||
transform: translate(-50%, -50%) scale(.8);
|
||||
opacity: 0;
|
||||
transition: .2s cubic-bezier(.5, -.55, .4, 1.55);
|
||||
|
||||
&.Modal-body-enter-active {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:global .Modal-body-leave {
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 1;
|
||||
transition: .14s;
|
||||
|
||||
&.Modal-body-leave-active {
|
||||
transform: translate(-50%, -60%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
35
src/js/components/Offline/index.js
Normal file
35
src/js/components/Offline/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
export default class Avatar extends Component {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.show) return false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.container}
|
||||
{...this.props}>
|
||||
<div>
|
||||
<img
|
||||
className="disabledDrag"
|
||||
src="assets/images/offline.png" />
|
||||
|
||||
<h1>Oops, seems like you are offline!</h1>
|
||||
|
||||
<button onClick={e => window.location.reload()}>Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
50
src/js/components/Offline/style.css
Normal file
50
src/js/components/Offline/style.css
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f9f9f9;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99;
|
||||
|
||||
& img {
|
||||
transform: translateY(60px);
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
color: #777;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
& button {
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
margin-top: 4%;
|
||||
padding: 2% 5%;
|
||||
border: solid 0 transparent;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26);
|
||||
background: #4285f4 radial-gradient(circle at 0 0, rgba(0, 0, 0, .3) 0%, rgba(0, 0, 0, 0) 0) no-repeat;
|
||||
background-color: #4285f4;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
& > div {
|
||||
transform: translateY(-110px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
top: 30px;
|
||||
}
|
||||
}
|
||||
44
src/js/components/Snackbar/index.js
Normal file
44
src/js/components/Snackbar/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './style.global.css';
|
||||
import TransitionPortal from '../TransitionPortal';
|
||||
|
||||
export default class Snackbar extends Component {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
renderContent() {
|
||||
if (!this.props.show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Snackbar">
|
||||
<div
|
||||
className="Snackbar-text"
|
||||
dangerouslySetInnerHTML={{__html: this.props.text}} />
|
||||
<div
|
||||
className="Snackbar-action"
|
||||
onClick={() => this.props.close()}>
|
||||
DONE
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TransitionPortal
|
||||
transitionEnterTimeout={0}
|
||||
transitionLeaveTimeout={150}
|
||||
transitionName="Snackbar">
|
||||
{this.renderContent()}
|
||||
</TransitionPortal>
|
||||
);
|
||||
}
|
||||
}
|
||||
71
src/js/components/Snackbar/style.global.css
Normal file
71
src/js/components/Snackbar/style.global.css
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
.Snackbar {
|
||||
position: fixed;
|
||||
right: 17px;
|
||||
bottom: 84px;
|
||||
display: flex;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
min-width: 288px;
|
||||
max-width: 568px;
|
||||
border-radius: 2px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 24px;
|
||||
font-family: 'Roboto';
|
||||
background-color: #323232;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.Snackbar-action {
|
||||
padding: 0 24px;
|
||||
color: #ff4081;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Snackbar-enter {
|
||||
transform: translateY(24px);
|
||||
opacity: 0;
|
||||
transition: .2s cubic-bezier(.5, -.55, .4, 1.55);
|
||||
}
|
||||
|
||||
.Snackbar-enter.Snackbar-enter-active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.Snackbar-leave {
|
||||
opacity: 1;
|
||||
transition: .14s;
|
||||
}
|
||||
|
||||
.Snackbar-leave.Snackbar-leave-active {
|
||||
transform: translateY(-24px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.Snackbar {
|
||||
right: 17px;
|
||||
bottom: 59px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
min-width: 220px;
|
||||
max-width: 460px;
|
||||
padding-left: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.Snackbar-action {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.Snackbar-enter {
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
.Snackbar-leave.Snackbar-leave-active {
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
}
|
||||
18
src/js/components/Switch/index.js
Normal file
18
src/js/components/Switch/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import blacklist from 'utils/blacklist';
|
||||
import './style.global.css';
|
||||
|
||||
export default class Switch extends Component {
|
||||
render() {
|
||||
return (
|
||||
<span className="Switch">
|
||||
<input
|
||||
{...blacklist(this.props, 'className', 'children')}
|
||||
type="checkbox" />
|
||||
<span className="Switch--fake" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
66
src/js/components/Switch/style.global.css
Normal file
66
src/js/components/Switch/style.global.css
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
.Switch {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.Switch input:checked + .Switch--fake::before {
|
||||
background: rgba(157, 166, 216, 1);
|
||||
}
|
||||
|
||||
.Switch input:checked + .Switch--fake::after {
|
||||
left: auto;
|
||||
right: 0;
|
||||
background: #3f51b5;
|
||||
}
|
||||
|
||||
.Switch--fake {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 35px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.Switch--fake::before,
|
||||
.Switch--fake::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.Switch--fake::before {
|
||||
display: block;
|
||||
width: 35px;
|
||||
height: 14px;
|
||||
margin-top: 3px;
|
||||
background: rgba(0, 0, 0, .26);
|
||||
border-radius: 14px;
|
||||
transition: .5s ease-in-out;
|
||||
}
|
||||
|
||||
.Switch--fake::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, .5);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.Switch input:disabled + .Switch--fake::before {
|
||||
background: rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.Switch input:disabled + .Switch--fake::after {
|
||||
left: auto;
|
||||
right: 0;
|
||||
background: #bdbdbd;
|
||||
}
|
||||
26
src/js/components/TransitionPortal/index.js
Normal file
26
src/js/components/TransitionPortal/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Transition from 'react-addons-css-transition-group';
|
||||
|
||||
export default class TransitionPortal extends Component {
|
||||
ele;
|
||||
|
||||
componentDidMount() {
|
||||
this.ele = document.createElement('div');
|
||||
document.body.appendChild(this.ele);
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
ReactDOM.render(<Transition {...this.props}>{this.props.children}</Transition>, this.ele);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.body.removeChild(this.ele);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
195
src/js/components/UserList/index.js
Normal file
195
src/js/components/UserList/index.js
Normal file
@@ -0,0 +1,195 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import clazz from 'classname';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
export default class UserList extends Component {
|
||||
static propTypes = {
|
||||
max: PropTypes.number.isRequired,
|
||||
searching: PropTypes.string.isRequired,
|
||||
search: PropTypes.func.isRequired,
|
||||
getList: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
max: 20,
|
||||
};
|
||||
|
||||
state = {
|
||||
selected: [],
|
||||
active: '',
|
||||
};
|
||||
|
||||
highlight(offset) {
|
||||
var scroller = this.refs.list;
|
||||
var users = Array.from(scroller.querySelectorAll('li[data-userid]'));
|
||||
var index = users.findIndex(e => e.classList.contains(classes.active));
|
||||
|
||||
if (index > -1) {
|
||||
users[index].classList.remove(classes.active);
|
||||
}
|
||||
|
||||
index += offset;
|
||||
|
||||
if (index < 0) {
|
||||
// Fallback to the last element
|
||||
index = users.length - 1;
|
||||
} else if (index > users.length - 1) {
|
||||
// Fallback to the 1th element
|
||||
index = 0;
|
||||
}
|
||||
|
||||
var active = users[index];
|
||||
|
||||
if (active) {
|
||||
// Keep active item always in the viewport
|
||||
active.classList.add(classes.active);
|
||||
scroller.scrollTop = active.offsetTop + active.offsetHeight - scroller.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
navigation(e) {
|
||||
var keyCode = e.keyCode;
|
||||
var offset = {
|
||||
// Up
|
||||
'38': -1,
|
||||
'40': 1,
|
||||
}[keyCode];
|
||||
|
||||
if (offset) {
|
||||
this.highlight(offset);
|
||||
}
|
||||
|
||||
if (keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
|
||||
var active = this.refs.list.querySelector(`.${classes.active}`);
|
||||
|
||||
if (active) {
|
||||
let userid = active.dataset.userid;
|
||||
|
||||
if (!this.state.selected.includes(userid)) {
|
||||
// Add
|
||||
this.addSelected(userid, userid);
|
||||
} else {
|
||||
// Remove
|
||||
this.removeSelected(userid, userid);
|
||||
}
|
||||
setTimeout(() => this.props.onChange(this.state.selected));
|
||||
}
|
||||
}
|
||||
|
||||
timer;
|
||||
|
||||
search(text) {
|
||||
clearTimeout(this.timer);
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.props.search(text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
addSelected(userid, active = this.state.active) {
|
||||
var selected = [
|
||||
userid,
|
||||
...this.state.selected,
|
||||
];
|
||||
var max = this.props.max;
|
||||
|
||||
if (max > 0) {
|
||||
selected = selected.slice(0, this.props.max);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active,
|
||||
selected,
|
||||
});
|
||||
setTimeout(() => this.props.onChange(this.state.selected));
|
||||
}
|
||||
|
||||
removeSelected(userid, active = this.state.active) {
|
||||
var selected = this.state.selected;
|
||||
var index = selected.indexOf(userid);
|
||||
|
||||
this.setState({
|
||||
active,
|
||||
selected: [
|
||||
...selected.slice(0, index),
|
||||
...selected.slice(index + 1, selected.length)
|
||||
]
|
||||
});
|
||||
setTimeout(() => this.props.onChange(this.state.selected));
|
||||
}
|
||||
|
||||
toggleSelected(userid) {
|
||||
if (!this.state.selected.includes(userid)) {
|
||||
// Add
|
||||
this.addSelected(userid);
|
||||
} else {
|
||||
// Remove
|
||||
this.removeSelected(userid);
|
||||
}
|
||||
|
||||
setTimeout(() => this.refs.input.focus());
|
||||
}
|
||||
|
||||
renderList() {
|
||||
var { searching, getList } = this.props;
|
||||
var list = getList();
|
||||
|
||||
if (searching && list.length === 0) {
|
||||
return (
|
||||
<li className={classes.notfound}>
|
||||
<img src="assets/images/crash.png" />
|
||||
<h3>Can't find any people matching '{searching}'</h3>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return list.map((e, index) => {
|
||||
return (
|
||||
<li
|
||||
className={clazz({
|
||||
[classes.selected]: this.state.selected.includes(e.UserName),
|
||||
[classes.active]: this.state.active === e.UserName,
|
||||
})}
|
||||
data-userid={e.UserName}
|
||||
key={index}
|
||||
onClick={ev => this.toggleSelected(e.UserName)}>
|
||||
<img
|
||||
className={classes.avatar}
|
||||
src={e.HeadImgUrl} />
|
||||
<span
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{__html: e.RemarkName || e.NickName}} />
|
||||
|
||||
<i className="icon-ion-android-done-all" />
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
onKeyUp={e => this.navigation(e)}
|
||||
onInput={e => this.search(e.target.value)}
|
||||
placeholder="Type to Search..."
|
||||
ref="input"
|
||||
type="text" />
|
||||
|
||||
<ul
|
||||
className={classes.list}
|
||||
ref="list">
|
||||
{this.renderList()}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
97
src/js/components/UserList/style.css
Normal file
97
src/js/components/UserList/style.css
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
.list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
height: 60vh;
|
||||
width: 45vw;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
& li {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 24px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.notfound):hover,
|
||||
&.active,
|
||||
&.active i {
|
||||
color: rgba(33, 150, 243, .9) !important;
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&.selected i {
|
||||
color: #3f51b5;
|
||||
}
|
||||
}
|
||||
|
||||
& li i {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 24px;
|
||||
font-size: 20px;
|
||||
color: #ddd;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 24px;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.notfound {
|
||||
justify-content: center !important;
|
||||
flex-direction: column;
|
||||
|
||||
& img {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
font-weight: 100;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.list {
|
||||
& li {
|
||||
margin: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
& li i {
|
||||
right: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.notfound {
|
||||
& img {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/js/pages/AddFriend/index.js
Normal file
47
src/js/pages/AddFriend/index.js
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Modal, ModalBody } from 'components/Modal';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
@inject(stores => ({
|
||||
me: stores.session.user,
|
||||
show: stores.addfriend.show,
|
||||
close: () => stores.addfriend.toggle(false),
|
||||
sendRequest: stores.addfriend.sendRequest,
|
||||
}))
|
||||
@observer
|
||||
export default class AddFriend extends Component {
|
||||
addFriend() {
|
||||
this.props.sendRequest(this.refs.input.value);
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
render() {
|
||||
var { me, show, close } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
fullscreen={true}
|
||||
onCancel={e => close()}
|
||||
show={show}>
|
||||
<ModalBody className={classes.container}>
|
||||
Send friend request first
|
||||
|
||||
<input
|
||||
autoFocus={true}
|
||||
defaultValue={`Hallo, im ${me && me.User.NickName}`}
|
||||
ref="input"
|
||||
type="text" />
|
||||
|
||||
<div>
|
||||
<button onClick={e => this.addFriend()}>Send</button>
|
||||
|
||||
<button onClick={e => close()}>Cancel</button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
76
src/js/pages/AddFriend/style.css
Normal file
76
src/js/pages/AddFriend/style.css
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
.container {
|
||||
background: rgba(0, 0, 0, .9);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Roboto';
|
||||
font-size: 36px;
|
||||
color: #fff;
|
||||
font-weight: 100;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
|
||||
& input {
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
margin-top: 5%;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 100;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
& button {
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
background-color: rgba(99, 99, 99, 0);
|
||||
font-family: 'Roboto';
|
||||
font-weight: 500;
|
||||
color: rgba(33, 150, 243, .9);
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
margin: 24px 12px;
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& button:hover {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
font-size: 26px;
|
||||
word-spacing: 2px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
& button {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 12px;
|
||||
margin: 24px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/js/pages/AddMember/index.js
Normal file
122
src/js/pages/AddMember/index.js
Normal file
@@ -0,0 +1,122 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Modal, ModalBody } from 'components/Modal';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
import UserList from 'components/UserList';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
@inject(stores => ({
|
||||
show: stores.addmember.show,
|
||||
searching: stores.addmember.query,
|
||||
getList: () => {
|
||||
var { addmember, contacts } = stores;
|
||||
|
||||
if (addmember.query) {
|
||||
return addmember.list;
|
||||
}
|
||||
|
||||
return contacts.memberList.filter(
|
||||
e => !helper.isChatRoom(e.UserName)
|
||||
&& !helper.isFileHelper(e)
|
||||
&& e.UserName !== stores.session.user.User.UserName
|
||||
);
|
||||
},
|
||||
addMember: async(userids) => {
|
||||
var roomid = stores.chat.user.UserName;
|
||||
|
||||
return stores.addmember.addMember(roomid, userids);
|
||||
},
|
||||
getUser: (userid) => {
|
||||
return stores.contacts.memberList.find(e => e.UserName === userid);
|
||||
},
|
||||
search: stores.addmember.search,
|
||||
close: () => {
|
||||
stores.addmember.reset();
|
||||
stores.addmember.toggle(false);
|
||||
},
|
||||
}))
|
||||
@observer
|
||||
export default class AddMember extends Component {
|
||||
state = {
|
||||
selected: [],
|
||||
};
|
||||
|
||||
close() {
|
||||
this.props.close();
|
||||
this.setState({
|
||||
selected: [],
|
||||
});
|
||||
}
|
||||
|
||||
async add(userids) {
|
||||
await this.props.addMember(userids);
|
||||
this.close();
|
||||
}
|
||||
|
||||
renderList() {
|
||||
var self = this;
|
||||
var { show, searching, search, getList } = this.props;
|
||||
|
||||
if (!show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserList {...{
|
||||
ref: 'users',
|
||||
|
||||
search,
|
||||
getList,
|
||||
searching,
|
||||
max: -1,
|
||||
|
||||
onChange(selected) {
|
||||
self.setState({
|
||||
selected,
|
||||
});
|
||||
}
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
fullscreen={true}
|
||||
onCancel={e => this.close()}
|
||||
show={this.props.show}>
|
||||
<ModalBody className={classes.container}>
|
||||
Add Members
|
||||
|
||||
<div className={classes.avatars}>
|
||||
{
|
||||
this.state.selected.map((e, index) => {
|
||||
var user = this.props.getUser(e);
|
||||
return (
|
||||
<img
|
||||
key={index}
|
||||
onClick={ev => this.refs.users.removeSelected(e)}
|
||||
src={user.HeadImgUrl} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{this.renderList()}
|
||||
|
||||
<div>
|
||||
<button
|
||||
disabled={!this.state.selected.length}
|
||||
onClick={e => this.add(this.state.selected)}>
|
||||
Add
|
||||
</button>
|
||||
|
||||
<button onClick={e => this.close()}>Cancel</button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
103
src/js/pages/AddMember/style.css
Normal file
103
src/js/pages/AddMember/style.css
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
.container {
|
||||
background: #fff;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Roboto';
|
||||
font-size: 36px;
|
||||
color: #000;
|
||||
font-weight: 100;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
|
||||
& input {
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
& button {
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
background-color: rgba(99, 99, 99, 0);
|
||||
font-family: 'Roboto';
|
||||
font-weight: 500;
|
||||
color: rgba(33, 150, 243, .9);
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
margin: 0 12px;
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& button:disabled {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
& button:hover {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
min-height: 20px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 12px 4px;
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, .3);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
font-size: 26px;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 2px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
& button {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 12px;
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
min-height: 20px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 12px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/js/pages/BatchSend/index.js
Normal file
174
src/js/pages/BatchSend/index.js
Normal file
@@ -0,0 +1,174 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import clazz from 'classname';
|
||||
|
||||
import classes from './style.css';
|
||||
import MessageInput from 'components/MessageInput';
|
||||
|
||||
@inject(stores => ({
|
||||
show: stores.batchsend.show,
|
||||
close: () => stores.batchsend.toggle(false),
|
||||
search: stores.batchsend.search,
|
||||
searching: stores.batchsend.query,
|
||||
contacts: stores.contacts.memberList,
|
||||
filtered: stores.batchsend.filtered,
|
||||
sendMessage: stores.chat.sendMessage,
|
||||
showMessage: stores.snackbar.showMessage,
|
||||
me: stores.session.user,
|
||||
confirmSendImage: async(image) => {
|
||||
if (!stores.settings.confirmImagePaste) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var confirmed = await stores.confirmImagePaste.toggle(true, image);
|
||||
return confirmed;
|
||||
},
|
||||
process: stores.chat.process,
|
||||
}))
|
||||
@observer
|
||||
export default class BatchSend extends Component {
|
||||
state = {
|
||||
selected: [],
|
||||
};
|
||||
|
||||
close() {
|
||||
this.setState({
|
||||
selected: [],
|
||||
});
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
selected: [],
|
||||
});
|
||||
this.props.search();
|
||||
}
|
||||
|
||||
handleSelected(user) {
|
||||
var selected = this.state.selected;
|
||||
var index = selected.findIndex(e => e.UserName === user.UserName);
|
||||
|
||||
if (index === -1) {
|
||||
selected.push(user);
|
||||
} else {
|
||||
selected = [
|
||||
...selected.slice(0, index),
|
||||
...selected.slice(index + 1, selected.length),
|
||||
];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selected,
|
||||
});
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
var contacts = this.props.contacts;
|
||||
var selected = this.state.selected;
|
||||
var isall = contacts.length === selected.length;
|
||||
|
||||
if (isall) {
|
||||
// Unselected all user
|
||||
selected = [];
|
||||
} else {
|
||||
selected = contacts.map(e => Object.assign({}, e));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selected,
|
||||
});
|
||||
}
|
||||
|
||||
search(text = '') {
|
||||
text = text.trim();
|
||||
|
||||
clearTimeout(this.search.timer);
|
||||
this.search.timer = setTimeout(() => {
|
||||
this.props.search(text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
render() {
|
||||
var { contacts, searching, filtered, showMessage, sendMessage, me = {}, confirmSendImage, process } = this.props;
|
||||
|
||||
if (!this.props.show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<header>
|
||||
<input
|
||||
autoFocus={true}
|
||||
onInput={e => this.search(e.target.value)}
|
||||
placeholder="Batch to send message, Choose one or more user."
|
||||
type="text" />
|
||||
|
||||
<span>
|
||||
<i
|
||||
className={clazz('icon-ion-android-done-all', {
|
||||
[classes.active]: this.state.selected.length === contacts.length
|
||||
})}
|
||||
onClick={() => this.selectAll()}
|
||||
style={{
|
||||
marginRight: 20,
|
||||
}} />
|
||||
<i
|
||||
className="icon-ion-android-close"
|
||||
onClick={e => this.close()} />
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<ul className={classes.list}>
|
||||
{
|
||||
(searching && filtered.length === 0) && (
|
||||
<div className={classes.notfound}>
|
||||
<img src="assets/images/crash.png" />
|
||||
<h1>Can't find any people matching '{searching}'</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(searching ? filtered : contacts).map((e, index) => {
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() => this.handleSelected(e)}>
|
||||
<div
|
||||
className={classes.cover}
|
||||
style={{
|
||||
backgroundImage: `url(${e.HeadImgUrl})`,
|
||||
}} />
|
||||
<span
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{ __html: e.RemarkName || e.NickName }} />
|
||||
|
||||
{
|
||||
this.state.selected.find(user => user.UserName === e.UserName) && (
|
||||
<i className="icon-ion-android-done" />
|
||||
)
|
||||
}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div className={classes.footer}>
|
||||
<MessageInput {...{
|
||||
className: classes.input,
|
||||
me: me.User,
|
||||
sendMessage,
|
||||
showMessage,
|
||||
user: this.state.selected,
|
||||
confirmSendImage,
|
||||
process,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
206
src/js/pages/BatchSend/style.css
Normal file
206
src/js/pages/BatchSend/style.css
Normal file
@@ -0,0 +1,206 @@
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
z-index: 9;
|
||||
|
||||
& header {
|
||||
display: flex;
|
||||
padding: 0 12px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 100;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
& input {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
border: 0;
|
||||
background: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
& header i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& header input {
|
||||
height: 50px;
|
||||
padding-right: 17px;
|
||||
line-height: 50px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
& .active,
|
||||
& header i:hover {
|
||||
color: #34b7f1;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
padding-top: 8px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
height: calc(100vh - 90px - 60px - 8px); /* Height - Header - Search Input - Message Input - Padding */
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
& li {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 4px 18px;
|
||||
padding-right: 32px;
|
||||
margin: 6px 8px;
|
||||
min-width: 80px;
|
||||
line-height: 32px;
|
||||
text-align: left;
|
||||
border-radius: 48px;
|
||||
color: #fff;
|
||||
background: #f7f7f7;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
|
||||
& li:hover .cover {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
& li i {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 24px;
|
||||
background: #405de6;
|
||||
}
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
transform: scale(1);
|
||||
opacity: .6;
|
||||
transition: .2s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, .3);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 60px;
|
||||
box-shadow: inset 0 1px 0 0 #eaedea;
|
||||
}
|
||||
|
||||
.notfound {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
& img {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-family: 'Roboto';
|
||||
font-weight: 100;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
top: 30px;
|
||||
|
||||
& header {
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
& header input {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
height: calc(100vh - 80px - 46px - 8px);
|
||||
|
||||
& li {
|
||||
padding: 2px 14px;
|
||||
padding-right: 24px;
|
||||
margin: 4px 6px;
|
||||
min-width: 70px;
|
||||
line-height: 28px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 46px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.notfound {
|
||||
& img {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
word-spacing: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/js/pages/ConfirmImagePaste/index.js
Normal file
65
src/js/pages/ConfirmImagePaste/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Modal, ModalBody } from 'components/Modal';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
@inject(stores => {
|
||||
var confirmImagePaste = stores.confirmImagePaste;
|
||||
|
||||
return {
|
||||
show: confirmImagePaste.show,
|
||||
image: confirmImagePaste.image,
|
||||
|
||||
ok: () => {
|
||||
console.log('ok');
|
||||
confirmImagePaste.ok();
|
||||
confirmImagePaste.toggle(false);
|
||||
},
|
||||
cancel: () => {
|
||||
console.log('cancel');
|
||||
confirmImagePaste.cancel();
|
||||
confirmImagePaste.toggle(false);
|
||||
},
|
||||
};
|
||||
})
|
||||
@observer
|
||||
export default class ConfirmImagePaste extends Component {
|
||||
navigation(e) {
|
||||
// User press ESC
|
||||
if (e.keyCode === 81) {
|
||||
console.log(81);
|
||||
this.props.cancel();
|
||||
}
|
||||
if (e.keyCode === 13) {
|
||||
console.log(13);
|
||||
this.props.ok();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var { show, cancel, ok, image } = this.props;
|
||||
setTimeout(() => {
|
||||
document.querySelector('#imageInputHidden').focus();
|
||||
}, 1000);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
fullscreen={true}
|
||||
show={show}>
|
||||
<ModalBody className={classes.container}>
|
||||
Send image ?
|
||||
|
||||
<img src={image} />
|
||||
|
||||
<div>
|
||||
<input onKeyUp={e => this.navigation(e)} id="imageInputHidden" style={{'zIndex': '-1', 'position': 'absolute', 'top': '-20px'}} />
|
||||
<button onClick={e => ok()}>Send</button>
|
||||
<button onClick={e => cancel()}>Cancel</button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
73
src/js/pages/ConfirmImagePaste/style.css
Normal file
73
src/js/pages/ConfirmImagePaste/style.css
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
.container {
|
||||
background: rgba(0, 0, 0, .9);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Roboto';
|
||||
font-size: 36px;
|
||||
color: #fff;
|
||||
font-weight: 100;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
|
||||
& button {
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
background-color: rgba(99, 99, 99, 0);
|
||||
font-family: 'Roboto';
|
||||
font-weight: 500;
|
||||
color: rgba(33, 150, 243, .9);
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
margin: 24px 12px;
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& button:hover {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: 70vw;
|
||||
max-height: 60vh;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
border: 12px solid #fff;
|
||||
border-radius: 1px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, .3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
font-size: 26px;
|
||||
word-spacing: 2px;
|
||||
|
||||
& button {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 12px;
|
||||
margin: 24px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: 54vw;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 20px;
|
||||
border: 6px solid #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/js/pages/Contacts/index.js
Normal file
113
src/js/pages/Contacts/index.js
Normal file
@@ -0,0 +1,113 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import clazz from 'classname';
|
||||
import randomColor from 'randomcolor';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
@inject(stores => ({
|
||||
filter: stores.contacts.filter,
|
||||
filtered: stores.contacts.filtered,
|
||||
getContats: stores.contacts.getContats,
|
||||
showUserinfo: stores.userinfo.toggle,
|
||||
}))
|
||||
@observer
|
||||
export default class Contacts extends Component {
|
||||
renderColumns(data, index) {
|
||||
var list = data.filter((e, i) => i % 3 === index);
|
||||
|
||||
return list.map((e, index) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.group}
|
||||
key={index}>
|
||||
<div className={classes.header}>
|
||||
<label>{e.prefix}</label>
|
||||
|
||||
<span>{e.list.length} people</span>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
height: 4,
|
||||
width: '100%',
|
||||
background: randomColor(),
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div className={classes.list}>
|
||||
{
|
||||
e.list.map((e, index) => {
|
||||
return (
|
||||
<div
|
||||
className={classes.item}
|
||||
key={index}
|
||||
onClick={() => this.props.showUserinfo(true, e)}>
|
||||
<div className={classes.avatar}>
|
||||
<img
|
||||
src={e.HeadImgUrl}
|
||||
style={{
|
||||
height: 32,
|
||||
width: 32,
|
||||
}} />
|
||||
</div>
|
||||
<div className={classes.info}>
|
||||
<p
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{__html: e.RemarkName || e.NickName}} />
|
||||
<p
|
||||
className={classes.signature}
|
||||
dangerouslySetInnerHTML={{__html: e.Signature || 'No Signature'}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.filter();
|
||||
}
|
||||
|
||||
render() {
|
||||
var { query, result } = this.props.filtered;
|
||||
|
||||
if (query && result.length === 0) {
|
||||
return (
|
||||
<div className={clazz(classes.container, classes.notfound)}>
|
||||
<div className={classes.inner}>
|
||||
<img src="assets/images/crash.png" />
|
||||
<h1>Can't find any people matching '{query}'</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<div className={classes.columns}>
|
||||
<div className={classes.column}>
|
||||
{
|
||||
this.renderColumns(result, 0)
|
||||
}
|
||||
</div>
|
||||
<div className={classes.column}>
|
||||
{
|
||||
this.renderColumns(result, 1)
|
||||
}
|
||||
</div>
|
||||
<div className={classes.column}>
|
||||
{
|
||||
this.renderColumns(result, 2)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
183
src/js/pages/Contacts/style.css
Normal file
183
src/js/pages/Contacts/style.css
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
padding: 30px 17px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notfound {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& h1 {
|
||||
margin-bottom: 25vh;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 100;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
text-align: center;
|
||||
|
||||
& img {
|
||||
width: 220px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 274px;
|
||||
}
|
||||
|
||||
.group {
|
||||
width: 274px;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 0 10px 0 rgba(119, 119, 119, 50%);
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 36px;
|
||||
|
||||
& label {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
bottom: 8px;
|
||||
font-family: 'Helvetica Neue';
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
& span {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 8px;
|
||||
display: block;
|
||||
font-family: 'Roboto';
|
||||
font-size: 12px;
|
||||
color: #9b9b9b;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
margin: 10px 0;
|
||||
padding: 0 10px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
& p {
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
&:hover p {
|
||||
color: #405de6 !important;
|
||||
}
|
||||
|
||||
& img {
|
||||
margin-top: 3px;
|
||||
margin-right: 19px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.info {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.username,
|
||||
.signature {
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-family: 'Helvetica';
|
||||
font-size: 14px;
|
||||
color: #4a4a4a;
|
||||
margin: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.signature {
|
||||
font-family: 'Helvetica';
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.columns {
|
||||
padding: 20px 17px 0;
|
||||
}
|
||||
|
||||
.notfound {
|
||||
& h1 {
|
||||
word-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 230px;
|
||||
}
|
||||
|
||||
.group {
|
||||
width: 230px;
|
||||
padding: 6px 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 110px;
|
||||
height: 26px;
|
||||
|
||||
& label {
|
||||
bottom: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
& span {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 36px;
|
||||
margin: 8px 0;
|
||||
padding: 0 8px;
|
||||
|
||||
& img {
|
||||
margin-top: 4px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.username,
|
||||
.signature {
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 13px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
56
src/js/pages/Footer/Contacts.js
Normal file
56
src/js/pages/Footer/Contacts.js
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { inject } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
import Switch from 'components/Switch';
|
||||
|
||||
@inject(stores => ({
|
||||
filter: stores.contacts.filter,
|
||||
showGroup: stores.contacts.showGroup,
|
||||
toggleGroup: stores.contacts.toggleGroup,
|
||||
}))
|
||||
export default class Filter extends Component {
|
||||
// Improve filter performance
|
||||
timer;
|
||||
|
||||
doFilter(text = '') {
|
||||
text = text.trim();
|
||||
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.props.filter(text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
handleShowGroup(e) {
|
||||
this.props.toggleGroup(e.target.checked);
|
||||
this.doFilter(this.refs.filter.value);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.filter();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={classes.contacts}>
|
||||
<input
|
||||
onInput={e => this.doFilter(e.target.value)}
|
||||
placeholder="Type something to search..."
|
||||
ref="filter"
|
||||
type="text" />
|
||||
|
||||
<div className={classes.action}>
|
||||
<label htmlFor="showGroup">
|
||||
<span className={classes.options}>Show Groups</span>
|
||||
<Switch
|
||||
defaultChecked={this.props.showGroup}
|
||||
id="showGroup"
|
||||
onClick={e => this.handleShowGroup(e)} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/js/pages/Footer/Home.js
Normal file
37
src/js/pages/Footer/Home.js
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { inject } from 'mobx-react';
|
||||
|
||||
import MessageInput from 'components/MessageInput';
|
||||
|
||||
@inject(stores => ({
|
||||
sendMessage: stores.chat.sendMessage,
|
||||
user: stores.chat.user,
|
||||
showMessage: stores.snackbar.showMessage,
|
||||
me: stores.session.user,
|
||||
confirmSendImage: async(image) => {
|
||||
if (!stores.settings.confirmImagePaste) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var confirmed = await stores.confirmImagePaste.toggle(true, image);
|
||||
return confirmed;
|
||||
},
|
||||
process: stores.chat.process,
|
||||
}))
|
||||
export default class Message extends Component {
|
||||
render() {
|
||||
var { sendMessage, showMessage, user, me = {}, confirmSendImage, process } = this.props;
|
||||
|
||||
return (
|
||||
<MessageInput {...{
|
||||
sendMessage,
|
||||
showMessage,
|
||||
user: user ? [user] : [],
|
||||
me: me.User,
|
||||
confirmSendImage,
|
||||
process,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/js/pages/Footer/Settings.js
Normal file
28
src/js/pages/Footer/Settings.js
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
export default class Placeholder extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={classes.settings}>
|
||||
<a
|
||||
className={classes.button}
|
||||
href="mailto:var.845541909@qq.com?Subject=WeWeChat%20Feedback"
|
||||
target="_blank">
|
||||
Send feedback
|
||||
<i className="icon-ion-ios-email-outline" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
className={classes.button}
|
||||
href="https://github.com/Riceneeder/weweChat"
|
||||
target="_blank">
|
||||
Fork on Github
|
||||
<i className="icon-ion-social-github" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
65
src/js/pages/Footer/index.js
Normal file
65
src/js/pages/Footer/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import clazz from 'classname';
|
||||
|
||||
import classes from './style.css';
|
||||
import Home from './Home';
|
||||
import Contacts from './Contacts';
|
||||
import Settings from './Settings';
|
||||
|
||||
export default class Footer extends Component {
|
||||
render() {
|
||||
var pathname = this.props.location.pathname;
|
||||
var component = {
|
||||
'/': Home,
|
||||
'/contacts': Contacts,
|
||||
'/settings': Settings,
|
||||
}[pathname];
|
||||
|
||||
return (
|
||||
<footer className={classes.footer}>
|
||||
<nav>
|
||||
<Link
|
||||
className="link"
|
||||
tabIndex="-1"
|
||||
to="/">
|
||||
<span className={clazz({
|
||||
[classes.active]: pathname === '/'
|
||||
})}>
|
||||
<i className="icon-ion-android-chat" />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="link"
|
||||
tabIndex="-1"
|
||||
to="/contacts">
|
||||
<span className={clazz({
|
||||
[classes.active]: pathname === '/contacts'
|
||||
})}>
|
||||
<i className="icon-ion-ios-book-outline" />
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="link"
|
||||
tabIndex="-1"
|
||||
to="/settings">
|
||||
<span className={clazz({
|
||||
[classes.active]: pathname === '/settings'
|
||||
})}>
|
||||
<i className="icon-ion-android-more-vertical" />
|
||||
</span>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className={classes.right}>
|
||||
{
|
||||
React.createElement(component)
|
||||
}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
201
src/js/pages/Footer/style.css
Normal file
201
src/js/pages/Footer/style.css
Normal file
@@ -0,0 +1,201 @@
|
||||
|
||||
:root {
|
||||
--icon-color: #777;
|
||||
--active: #34b7f1;
|
||||
--shadow-color: #eaedea;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 1px 0 0 var(--shadow-color);
|
||||
z-index: 9;
|
||||
|
||||
& nav {
|
||||
display: flex;
|
||||
width: calc(311px - 34px);
|
||||
height: 60px;
|
||||
padding: 17px 17px 0;
|
||||
justify-content: space-between;
|
||||
box-shadow: inset -1px 0 0 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
& nav span {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 27px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& nav i {
|
||||
color: var(--icon-color);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& nav span:hover i {
|
||||
color: var(--active) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.footer .active {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--active);
|
||||
}
|
||||
|
||||
& i {
|
||||
color: var(--active) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: calc(100% - 311px);
|
||||
}
|
||||
|
||||
.contacts {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& input {
|
||||
height: 60px;
|
||||
width: calc(100% - 200px);
|
||||
margin-left: 32px;
|
||||
line-height: 60px;
|
||||
border: 0;
|
||||
background: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
& label {
|
||||
display: flex;
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
& .action {
|
||||
margin-right: 17px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
display: flex;
|
||||
height: 60px;
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
& .button {
|
||||
position: relative;
|
||||
margin-right: 17px;
|
||||
width: 166px;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
font-size: 14px;
|
||||
padding: 9px 8px;
|
||||
font-family: 'Roboto';
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
background: 0;
|
||||
outline: 0;
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transform: translateY(4px);
|
||||
transition: .2s;
|
||||
|
||||
&:hover {
|
||||
background: color(var(--shadow-color) alpha(-50%));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
& .button i {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
font-family: 'Roboto';
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.footer {
|
||||
& nav {
|
||||
width: calc(280px - 34px);
|
||||
padding: 14px 17px 0;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
& nav span {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
& nav i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: calc(100% - 280px);
|
||||
}
|
||||
|
||||
.contacts {
|
||||
height: 46px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
width: calc(100% - 200px);
|
||||
margin-left: 20px;
|
||||
line-height: 46px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
height: 46px;
|
||||
|
||||
& .button {
|
||||
margin-right: 8px;
|
||||
width: 130px;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
& .button i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
112
src/js/pages/Forward/index.js
Normal file
112
src/js/pages/Forward/index.js
Normal file
@@ -0,0 +1,112 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Modal, ModalBody } from 'components/Modal';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
import UserList from 'components/UserList';
|
||||
|
||||
@inject(stores => ({
|
||||
show: stores.forward.show,
|
||||
searching: stores.forward.query,
|
||||
getList: () => {
|
||||
var { forward, contacts } = stores;
|
||||
|
||||
if (forward.query) {
|
||||
return forward.list;
|
||||
}
|
||||
|
||||
return contacts.memberList.filter(e => e.UserName !== stores.session.user.User.UserName);
|
||||
},
|
||||
getUser: (userid) => {
|
||||
return stores.contacts.memberList.find(e => e.UserName === userid);
|
||||
},
|
||||
search: stores.forward.search,
|
||||
send: (userids) => stores.forward.send(userids),
|
||||
close: () => stores.forward.toggle(false),
|
||||
}))
|
||||
@observer
|
||||
export default class Forward extends Component {
|
||||
state = {
|
||||
selected: [],
|
||||
};
|
||||
|
||||
close() {
|
||||
this.props.close();
|
||||
this.setState({
|
||||
selected: [],
|
||||
});
|
||||
}
|
||||
|
||||
send(userids) {
|
||||
userids.map(e => {
|
||||
this.props.send(e);
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
renderList() {
|
||||
var self = this;
|
||||
var { show, searching, search, getList } = this.props;
|
||||
|
||||
if (!show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserList {...{
|
||||
ref: 'users',
|
||||
|
||||
search,
|
||||
getList,
|
||||
searching,
|
||||
max: -1,
|
||||
|
||||
onChange(selected) {
|
||||
self.setState({
|
||||
selected,
|
||||
});
|
||||
}
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
fullscreen={true}
|
||||
onCancel={e => this.close()}
|
||||
show={this.props.show}>
|
||||
<ModalBody className={classes.container}>
|
||||
Forward Message
|
||||
|
||||
<div className={classes.avatars}>
|
||||
{
|
||||
this.state.selected.map((e, index) => {
|
||||
var user = this.props.getUser(e);
|
||||
return (
|
||||
<img
|
||||
key={index}
|
||||
onClick={ev => this.refs.users.removeSelected(e)}
|
||||
src={user.HeadImgUrl} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{this.renderList()}
|
||||
|
||||
<div>
|
||||
<button
|
||||
disabled={!this.state.selected.length}
|
||||
onClick={e => this.send(this.state.selected)}>
|
||||
Send Message
|
||||
</button>
|
||||
|
||||
<button onClick={e => this.close()}>Cancel</button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
103
src/js/pages/Forward/style.css
Normal file
103
src/js/pages/Forward/style.css
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
.container {
|
||||
background: #fff;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Roboto';
|
||||
font-size: 36px;
|
||||
color: #000;
|
||||
font-weight: 100;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
|
||||
& input {
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
& button {
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
background-color: rgba(99, 99, 99, 0);
|
||||
font-family: 'Roboto';
|
||||
font-weight: 500;
|
||||
color: rgba(33, 150, 243, .9);
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
margin: 0 12px;
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& button:disabled {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
& button:hover {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
min-height: 20px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 12px 4px;
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, .3);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
font-size: 26px;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 2px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
& button {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 12px;
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
min-height: 20px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 12px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/js/pages/Header/index.js
Normal file
27
src/js/pages/Header/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
export default class Header extends Component {
|
||||
getTitle() {
|
||||
switch (this.props.location.pathname) {
|
||||
case '/contacts':
|
||||
return 'Contacts - WeWeChat';
|
||||
|
||||
case '/settings':
|
||||
return 'Settings - WeWeChat';
|
||||
|
||||
default:
|
||||
return 'WeWeChat';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<header className={classes.container}>
|
||||
<h1>{this.getTitle()}</h1>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/js/pages/Header/style.css
Normal file
23
src/js/pages/Header/style.css
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
.container h1 {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-family: "system-ui";
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
color: #777;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container h1 {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
686
src/js/pages/Home/ChatContent/index.js
Normal file
686
src/js/pages/Home/ChatContent/index.js
Normal file
@@ -0,0 +1,686 @@
|
||||
|
||||
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(':<br/>');
|
||||
|
||||
// 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 `
|
||||
<img class="open-map unload" data-map="${message.location.href}" src="${message.location.image}" />
|
||||
<label>${message.location.label}</label>
|
||||
`;
|
||||
}
|
||||
// Text message
|
||||
return emojiParse(message.Content);
|
||||
case 3:
|
||||
// Image
|
||||
let image = message.image;
|
||||
|
||||
if (uploading) {
|
||||
return `
|
||||
<div>
|
||||
<img class="open-image unload" data-id="${message.MsgId}" src="${image.src}" data-fallback="${image.fallback}" />
|
||||
<i class="icon-ion-android-arrow-up"></i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `<img class="open-image unload" data-id="${message.MsgId}" src="${image.src}" data-fallback="${image.fallback}" />`;
|
||||
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 `
|
||||
<div class="play-voice" style="width: ${width}px" data-voice="${voice.src}">
|
||||
<i class="icon-ion-android-volume-up"></i>
|
||||
<span>
|
||||
${seconds || '60+'}"
|
||||
</span>
|
||||
|
||||
<audio controls="controls">
|
||||
<source src="${voice.src}" />
|
||||
</audio>
|
||||
</div>
|
||||
`;
|
||||
case 47:
|
||||
case 49 + 8:
|
||||
// External emoji
|
||||
let emoji = message.emoji;
|
||||
|
||||
if (emoji) {
|
||||
if (uploading) {
|
||||
return `
|
||||
<div>
|
||||
<img class="unload disabledDrag" src="${emoji.src}" data-fallback="${emoji.fallback}" />
|
||||
<i class="icon-ion-android-arrow-up"></i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `<img src="${emoji.src}" class="unload disabledDrag" data-fallback="${emoji.fallback}" />`;
|
||||
}
|
||||
return `
|
||||
<div class="${classes.invalidEmoji}">
|
||||
<div></div>
|
||||
<span>Send an emoji, view it on mobile</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 42:
|
||||
// Contact Card
|
||||
let contact = message.contact;
|
||||
let isFriend = this.props.isFriend(contact.UserName);
|
||||
let html = `
|
||||
<div class="${clazz(classes.contact, { 'is-friend': isFriend })}" data-userid="${contact.UserName}">
|
||||
<img src="${contact.image}" class="unload disabledDrag" />
|
||||
|
||||
<div>
|
||||
<p>${contact.name}</p>
|
||||
<p>${contact.address}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!isFriend) {
|
||||
html += `
|
||||
<i class="icon-ion-android-add" data-userid="${contact.UserName}"></i>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
|
||||
case 43:
|
||||
// Video message
|
||||
let video = message.video;
|
||||
|
||||
if (uploading) {
|
||||
return `
|
||||
<div>
|
||||
<video preload="metadata" controls src="${video.src}"></video>
|
||||
|
||||
<i class="icon-ion-android-arrow-up"></i>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!video) {
|
||||
console.error('Invalid video message: %o', message);
|
||||
|
||||
return `
|
||||
Receive an invalid video message, please see the console output.
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<video preload="metadata" poster="${video.cover}" controls src="${video.src}" />
|
||||
`;
|
||||
|
||||
case 49 + 2000:
|
||||
// Money transfer
|
||||
let transfer = message.transfer;
|
||||
|
||||
return `
|
||||
<div class="${classes.transfer}">
|
||||
<h4>Money Transfer</h4>
|
||||
<span>💰 ${transfer.money}</span>
|
||||
<p>如需收钱,请打开手机微信确认收款。</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 49 + 6:
|
||||
// File message
|
||||
let file = message.file;
|
||||
let download = message.download;
|
||||
|
||||
/* eslint-disable */
|
||||
return `
|
||||
<div class="${classes.file}" data-id="${message.MsgId}">
|
||||
|
||||
<img src="assets/images/filetypes/${helper.getFiletypeIcon(file.extension)}" class="disabledDrag" />
|
||||
|
||||
<div>
|
||||
<p>${file.name}</p>
|
||||
<p>${helper.humanSize(file.size)}</p>
|
||||
</div>
|
||||
|
||||
${
|
||||
uploading
|
||||
? '<i class="icon-ion-android-arrow-up"></i>'
|
||||
: (download.done ? '<i class="icon-ion-android-more-horizontal is-file"></i>' : '<i class="icon-ion-android-arrow-down is-download"></i>')
|
||||
}
|
||||
<div class="logding" style="${uploading ? 'display: block' : 'display: none'}">
|
||||
<div class="spinner">
|
||||
<div class="double-bounce1"></div>
|
||||
<div class="double-bounce2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
/* eslint-enable */
|
||||
|
||||
case 49 + 17:
|
||||
// Location sharing...
|
||||
return `
|
||||
<div class="${classes.locationSharing}">
|
||||
<i class="icon-ion-ios-location"></i>
|
||||
Location sharing, Please check your phone.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
key={index}
|
||||
className={clazz('unread', classes.message, classes.system)}
|
||||
dangerouslySetInnerHTML={{__html: e.Content}} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clazz('unread', classes.message, {
|
||||
// File is uploading
|
||||
[classes.uploading]: message.uploading === true,
|
||||
|
||||
[classes.isme]: message.isme,
|
||||
[classes.isText]: type === 1 && !message.location,
|
||||
[classes.isLocation]: type === 1 && message.location,
|
||||
[classes.isImage]: type === 3,
|
||||
[classes.isEmoji]: type === 47 || type === 49 + 8,
|
||||
[classes.isVoice]: type === 34,
|
||||
[classes.isContact]: type === 42,
|
||||
[classes.isVideo]: type === 43,
|
||||
|
||||
// App messages
|
||||
[classes.appMessage]: [49 + 2000, 49 + 17, 49 + 6].includes(type),
|
||||
[classes.isTransfer]: type === 49 + 2000,
|
||||
[classes.isLocationSharing]: type === 49 + 17,
|
||||
[classes.isFile]: type === 49 + 6,
|
||||
})} key={index}>
|
||||
<div>
|
||||
<Avatar
|
||||
src={message.isme ? message.HeadImgUrl : user.HeadImgUrl}
|
||||
className={classes.avatar}
|
||||
onClick={ev => this.props.showUserinfo(message.isme, user)}
|
||||
/>
|
||||
|
||||
<p
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{__html: user.DisplayName || user.RemarkName || user.NickName}}
|
||||
/>
|
||||
|
||||
<div className={classes.content}>
|
||||
<p
|
||||
onContextMenu={e => this.showMessageAction(message)}
|
||||
dangerouslySetInnerHTML={{__html: this.getMessageContent(message)}} />
|
||||
|
||||
<span className={classes.times}>{ moment(message.CreateTime * 1000).fromNow() }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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: 'Delete',
|
||||
click: () => {
|
||||
this.props.deleteMessage(message.MsgId);
|
||||
}
|
||||
},
|
||||
];
|
||||
var menu;
|
||||
|
||||
if (caniforward) {
|
||||
templates.unshift({
|
||||
label: 'Forward',
|
||||
click: () => {
|
||||
this.props.showForward(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (message.isme
|
||||
&& message.CreateTime - new Date() < 2 * 60 * 1000) {
|
||||
templates.unshift({
|
||||
label: 'Recall',
|
||||
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: 'Toggle the conversation',
|
||||
click: () => {
|
||||
this.props.toggleConversation();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Empty Content',
|
||||
click: () => {
|
||||
this.props.empty(user);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: helper.isTop(user) ? 'Unsticky' : 'Sticky on Top',
|
||||
click: () => {
|
||||
this.props.sticky(user);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
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 = `You has ${counter} unread messages.`;
|
||||
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 = `You has ${counter} unread messages.`;
|
||||
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 (
|
||||
<div
|
||||
className={clazz(classes.container, {
|
||||
[classes.hideConversation]: !showConversation,
|
||||
})}
|
||||
onClick={e => this.handleClick(e)}>
|
||||
{
|
||||
user ? (
|
||||
<div>
|
||||
<header>
|
||||
<div className={classes.info}>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{__html: title}}
|
||||
title={title} />
|
||||
|
||||
<span
|
||||
className={classes.signature}
|
||||
dangerouslySetInnerHTML={{__html: signature || 'No Signature'}}
|
||||
onClick={e => this.props.showMembers(user)}
|
||||
title={signature} />
|
||||
</div>
|
||||
|
||||
<i
|
||||
className="icon-ion-android-more-vertical"
|
||||
onClick={() => this.showMenu()} />
|
||||
</header>
|
||||
|
||||
<div
|
||||
className={classes.messages}
|
||||
onScroll={e => this.handleScroll(e)}
|
||||
ref="viewport">
|
||||
{
|
||||
this.renderMessages(messages.get(user.UserName), user)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={clazz({
|
||||
[classes.noselected]: !user,
|
||||
})}>
|
||||
<img
|
||||
className="disabledDrag"
|
||||
src="assets/images/noselected.png" />
|
||||
<h1>No Chat selected :(</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
className={classes.tips}
|
||||
ref="tips">
|
||||
Unread message.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
1021
src/js/pages/Home/ChatContent/style.css
Normal file
1021
src/js/pages/Home/ChatContent/style.css
Normal file
File diff suppressed because it is too large
Load Diff
166
src/js/pages/Home/Chats/index.js
Normal file
166
src/js/pages/Home/Chats/index.js
Normal file
@@ -0,0 +1,166 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { remote } from 'electron';
|
||||
import clazz from 'classname';
|
||||
import moment from 'moment';
|
||||
|
||||
import classes from './style.css';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
moment.updateLocale('en', {
|
||||
relativeTime: {
|
||||
past: '%s',
|
||||
m: '1 min',
|
||||
mm: '%d mins',
|
||||
h: 'an hour',
|
||||
hh: '%d h',
|
||||
s: 'now',
|
||||
ss: '%d s',
|
||||
},
|
||||
});
|
||||
|
||||
@inject(stores => ({
|
||||
chats: stores.chat.sessions,
|
||||
chatTo: stores.chat.chatTo,
|
||||
selected: stores.chat.user,
|
||||
messages: stores.chat.messages,
|
||||
markedRead: stores.chat.markedRead,
|
||||
sticky: stores.chat.sticky,
|
||||
removeChat: stores.chat.removeChat,
|
||||
loading: stores.session.loading,
|
||||
searching: stores.search.searching,
|
||||
}))
|
||||
@observer
|
||||
export default class Chats extends Component {
|
||||
getTheLastestMessage(userid) {
|
||||
var list = this.props.messages.get(userid);
|
||||
var res;
|
||||
|
||||
if (list) {
|
||||
// Make sure all chatset has be loaded
|
||||
res = list.data.slice(-1)[0];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
hasUnreadMessage(userid) {
|
||||
var list = this.props.messages.get(userid);
|
||||
|
||||
if (list) {
|
||||
return list.data.length !== (list.unread || 0);
|
||||
}
|
||||
}
|
||||
|
||||
showContextMenu(user) {
|
||||
var menu = new remote.Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Send Message',
|
||||
click: () => {
|
||||
this.props.chatTo(user);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: helper.isTop(user) ? 'Unsticky' : 'Sticky on Top',
|
||||
click: () => {
|
||||
this.props.sticky(user);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
click: () => {
|
||||
this.props.removeChat(user);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Mark as Read',
|
||||
click: () => {
|
||||
this.props.markedRead(user.UserName);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
menu.popup(remote.getCurrentWindow());
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
var container = this.refs.container;
|
||||
var active = container.querySelector(`.${classes.chat}.${classes.active}`);
|
||||
|
||||
if (active) {
|
||||
let rect4active = active.getBoundingClientRect();
|
||||
let rect4viewport = container.getBoundingClientRect();
|
||||
|
||||
// Keep the conversation always in the viewport
|
||||
if (!(rect4active.top >= rect4viewport.top
|
||||
&& rect4active.bottom <= rect4viewport.bottom)) {
|
||||
active.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var { loading, chats, selected, chatTo, searching } = this.props;
|
||||
|
||||
if (loading) return false;
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<div
|
||||
className={classes.chats}
|
||||
ref="container">
|
||||
{
|
||||
!searching && chats.map((e, index) => {
|
||||
var message = this.getTheLastestMessage(e.UserName) || {};
|
||||
var muted = helper.isMuted(e);
|
||||
var isTop = helper.isTop(e);
|
||||
return (
|
||||
<div
|
||||
className={clazz(classes.chat, {
|
||||
[classes.sticky]: isTop,
|
||||
[classes.active]: selected && selected.UserName === e.UserName
|
||||
})}
|
||||
key={index}
|
||||
onContextMenu={ev => this.showContextMenu(e)}
|
||||
onClick={ev => chatTo(e)}>
|
||||
<div className={classes.inner}>
|
||||
<div className={clazz(classes.dot, {
|
||||
[classes.green]: !muted && this.hasUnreadMessage(e.UserName),
|
||||
[classes.red]: muted && this.hasUnreadMessage(e.UserName)
|
||||
})}>
|
||||
<img
|
||||
className="disabledDrag"
|
||||
src={e.HeadImgUrl}
|
||||
onError={e => (e.target.src = 'assets/images/user-fallback.png')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={classes.info}>
|
||||
<p
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{__html: e.RemarkName || e.NickName}} />
|
||||
|
||||
<span
|
||||
className={classes.message}
|
||||
dangerouslySetInnerHTML={{__html: helper.getMessageContent(message) || 'No Message'}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className={classes.times}>
|
||||
{
|
||||
message.CreateTime ? moment(message.CreateTime * 1000).fromNow() : ''
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
154
src/js/pages/Home/Chats/style.css
Normal file
154
src/js/pages/Home/Chats/style.css
Normal file
@@ -0,0 +1,154 @@
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chats {
|
||||
height: calc(100vh - 160px); /* Height - Header - Footer - Search Input */
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 48px;
|
||||
padding: 13px 16px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
& img {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 48px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background: #e8edf3;
|
||||
}
|
||||
|
||||
&.sticky::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
margin-left: 16px;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.username {
|
||||
max-width: 170px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-right: 4px;
|
||||
margin-bottom: 12px;
|
||||
font-family: 'Helvetica';
|
||||
font-size: 15px;
|
||||
color: rgba(74, 74, 74, .9);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 170px;
|
||||
font-size: 14px;
|
||||
color: #9b9b9b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.times {
|
||||
font-size: 12px;
|
||||
color: #979797;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -12px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&.green::after {
|
||||
content: '';
|
||||
background: url(../../../../assets/images/messageGreen.png) 0 0 no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&.red::after {
|
||||
content: '';
|
||||
background: url(../../../../assets/images/messageRed.png) 0 0 no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.chats {
|
||||
height: calc(100vh - 86px - 46px);
|
||||
}
|
||||
|
||||
.chat {
|
||||
height: 36px;
|
||||
padding: 8px 16px;
|
||||
|
||||
& img {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
&.sticky::before {
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.username {
|
||||
max-width: 160px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 160px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.times {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
&::after {
|
||||
bottom: 3px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
232
src/js/pages/Home/SearchBar/index.js
Normal file
232
src/js/pages/Home/SearchBar/index.js
Normal file
@@ -0,0 +1,232 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
@inject(stores => ({
|
||||
history: stores.search.history,
|
||||
searching: stores.search.searching,
|
||||
toggle: stores.search.toggle,
|
||||
filter: stores.search.filter,
|
||||
result: stores.search.result,
|
||||
getPlaceholder: () => {
|
||||
stores.contacts.filter('', true);
|
||||
return stores.contacts.filtered.result;
|
||||
},
|
||||
chat: async(user) => {
|
||||
stores.chat.chatTo(user);
|
||||
stores.search.reset();
|
||||
await stores.search.addHistory(user);
|
||||
},
|
||||
clear: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
stores.search.clearHistory();
|
||||
stores.search.reset();
|
||||
}
|
||||
}))
|
||||
@observer
|
||||
export default class SearchBar extends Component {
|
||||
timer;
|
||||
|
||||
filter(text = '') {
|
||||
text = text.trim();
|
||||
|
||||
clearTimeout(this.filter.timer);
|
||||
this.filter.timer = setTimeout(() => {
|
||||
this.props.filter(text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
handleBlur(value) {
|
||||
setTimeout(() => {
|
||||
if (!value) {
|
||||
this.props.toggle(false);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
chatTo(user) {
|
||||
this.props.chat(user);
|
||||
this.refs.search.value = '';
|
||||
document.querySelector('#messageInput').focus();
|
||||
}
|
||||
|
||||
highlight(offset) {
|
||||
var scroller = this.refs.dropdown;
|
||||
var users = Array.from(scroller.querySelectorAll(`.${classes.user}`));
|
||||
var index = users.findIndex(e => e.classList.contains(classes.active));
|
||||
|
||||
if (index > -1) {
|
||||
users[index].classList.remove(classes.active);
|
||||
}
|
||||
|
||||
index += offset;
|
||||
|
||||
if (index < 0) {
|
||||
// Fallback to the last element
|
||||
index = users.length - 1;
|
||||
} else if (index > users.length - 1) {
|
||||
// Fallback to the 1th element
|
||||
index = 0;
|
||||
}
|
||||
|
||||
var active = users[index];
|
||||
|
||||
if (active) {
|
||||
// Keep active item always in the viewport
|
||||
active.classList.add(classes.active);
|
||||
scroller.scrollTop = active.offsetTop + active.offsetHeight - scroller.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
navigation(e) {
|
||||
var { result, history, getPlaceholder } = this.props;
|
||||
|
||||
// User press ESC
|
||||
if (e.keyCode === 27) {
|
||||
e.target.blur();
|
||||
}
|
||||
|
||||
if (![
|
||||
38, // Up
|
||||
40, // Down
|
||||
13, // Enter
|
||||
].includes(e.keyCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 38:
|
||||
// Up
|
||||
this.highlight(-1);
|
||||
break;
|
||||
|
||||
case 40:
|
||||
// Down
|
||||
this.highlight(1);
|
||||
break;
|
||||
|
||||
case 13:
|
||||
let active = this.refs.dropdown.querySelector(`.${classes.user}.${classes.active}`);
|
||||
|
||||
if (!active) {
|
||||
break;
|
||||
}
|
||||
this.chatTo([...result.friend, ...result.groups, ...history, ...getPlaceholder()].find(e => e.UserName === active.dataset.userid));
|
||||
}
|
||||
}
|
||||
|
||||
renderUser(user) {
|
||||
return (
|
||||
<div
|
||||
className={classes.user}
|
||||
onClick={e => this.chatTo(user)} data-userid={user.UserName}>
|
||||
<img src={user.HeadImgUrl} />
|
||||
|
||||
<div className={classes.info}>
|
||||
<p
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{__html: user.RemarkName || user.NickName}} />
|
||||
|
||||
<span
|
||||
className={classes.signature}
|
||||
dangerouslySetInnerHTML={{__html: user.Signature || 'No Signature'}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderList(list, title) {
|
||||
if (!list.length) return false;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h3>{title}</h3>
|
||||
</header>
|
||||
{
|
||||
list.map((e, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{this.renderUser(e)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHistory(list) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h3>History</h3>
|
||||
|
||||
<a
|
||||
href=""
|
||||
onClick={e => this.props.clear(e)}>
|
||||
CLEAR
|
||||
</a>
|
||||
</header>
|
||||
{
|
||||
list.map((e, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{this.renderUser(e)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPlaceholder() {
|
||||
var list = this.props.getPlaceholder();
|
||||
|
||||
return list.map((e, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{this.renderList(e.list, e.prefix)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var { searching, history, result } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<i className="icon-ion-ios-search-strong" />
|
||||
<input
|
||||
id="search"
|
||||
onBlur={e => this.handleBlur(e.target.value)}
|
||||
onFocus={e => this.filter(e.target.value)}
|
||||
onInput={e => this.filter(e.target.value)}
|
||||
onKeyUp={e => this.navigation(e)}
|
||||
placeholder="Search ..."
|
||||
ref="search"
|
||||
type="text" />
|
||||
{
|
||||
searching && (
|
||||
<div
|
||||
className={classes.dropdown}
|
||||
ref="dropdown">
|
||||
{
|
||||
!result.query && (history.length ? this.renderHistory(history) : this.renderPlaceholder())
|
||||
}
|
||||
|
||||
{this.renderList(result.friend, 'Friend')}
|
||||
{this.renderList(result.groups, 'Group')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
176
src/js/pages/Home/SearchBar/style.css
Normal file
176
src/js/pages/Home/SearchBar/style.css
Normal file
@@ -0,0 +1,176 @@
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
box-shadow: inset 0 -1px 0 0 #eaedea;
|
||||
|
||||
& input {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
line-height: 60px;
|
||||
background: none;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
font-family: 'Helvetica';
|
||||
font-size: 24px;
|
||||
font-weight: 100;
|
||||
text-indent: 62px;
|
||||
}
|
||||
|
||||
& i {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
font-size: 28px;
|
||||
color: #d9d9d9;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
height: calc(100vh - 100px - 60px); /* Height - Header - Footer - Search input */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
& header {
|
||||
display: flex;
|
||||
padding: 13px 16px;
|
||||
color: #777;
|
||||
font-family: 'Roboto';
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
& header h3 {
|
||||
margin: 0;
|
||||
font-family: 'Helvetica';
|
||||
font-weight: 100;
|
||||
color: #000;
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 0 4px 2px 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
& header a {
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
color: #405de6;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 13px 16px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
& img {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background: #e8edf3;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
max-width: 170px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 4px;
|
||||
font-family: 'Helvetica';
|
||||
font-size: 15px;
|
||||
color: rgba(74, 74, 74, .9);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.signature {
|
||||
max-width: 170px;
|
||||
font-size: 14px;
|
||||
color: #9b9b9b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
height: 46px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 16px;
|
||||
text-indent: 36px;
|
||||
}
|
||||
|
||||
& i {
|
||||
left: 14px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
top: 46px;
|
||||
height: calc(100vh - 86px - 46px);
|
||||
|
||||
& header {
|
||||
padding: 6px 16px;
|
||||
}
|
||||
|
||||
& header h3 {
|
||||
padding: 0 8px 2px 0;
|
||||
}
|
||||
|
||||
& header a {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
padding: 6px 16px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
max-width: 160px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.signature {
|
||||
max-width: 160px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
56
src/js/pages/Home/index.js
Normal file
56
src/js/pages/Home/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import clazz from 'classname';
|
||||
|
||||
import classes from './style.css';
|
||||
import Loader from 'components/Loader';
|
||||
import SearchBar from './SearchBar';
|
||||
import Chats from './Chats';
|
||||
import ChatContent from './ChatContent';
|
||||
|
||||
@inject(stores => ({
|
||||
loading: stores.session.loading,
|
||||
showConversation: stores.chat.showConversation,
|
||||
toggleConversation: stores.chat.toggleConversation,
|
||||
showRedIcon: stores.settings.showRedIcon,
|
||||
newChat: () => stores.newchat.toggle(true),
|
||||
}))
|
||||
@observer
|
||||
export default class Home extends Component {
|
||||
componentDidMount() {
|
||||
this.props.toggleConversation(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<Loader
|
||||
fullscreen={true}
|
||||
show={this.props.loading} />
|
||||
<div className={clazz(classes.inner, {
|
||||
[classes.hideConversation]: !this.props.showConversation
|
||||
})}>
|
||||
<div className={classes.left}>
|
||||
<SearchBar />
|
||||
<Chats />
|
||||
|
||||
{
|
||||
this.props.showRedIcon && (
|
||||
<div
|
||||
className={classes.addChat}
|
||||
onClick={() => this.props.newChat()}>
|
||||
<i className="icon-ion-android-add" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={classes.right}>
|
||||
<ChatContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
60
src/js/pages/Home/style.css
Normal file
60
src/js/pages/Home/style.css
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.inner.hideConversation {
|
||||
& .left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .right {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
position: relative;
|
||||
width: 311px;
|
||||
box-shadow: inset -1px 0 0 0 #eaedea;
|
||||
|
||||
& .addChat {
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 40px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 100%;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
background: #e1306c;
|
||||
box-shadow: 0 0 24px 0 rgba(119, 119, 119, .5);
|
||||
cursor: pointer;
|
||||
z-index: 9;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: calc(100vw - 311px);
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.left {
|
||||
width: 280px;
|
||||
|
||||
& .addChat {
|
||||
right: -15px;
|
||||
top: 30px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: calc(100vw - 280px);
|
||||
}
|
||||
}
|
||||
68
src/js/pages/Layout.css
Normal file
68
src/js/pages/Layout.css
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
.container {
|
||||
height: calc(100vh - 100px);
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
background: rgba(255, 255, 255, .8);
|
||||
box-shadow: inset 0 1px 0 0 #eaedea;
|
||||
filter: blur(0);
|
||||
transition: .2s;
|
||||
|
||||
&.blur {
|
||||
filter: blur(6px);
|
||||
}
|
||||
}
|
||||
|
||||
.dragDropHolder {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
font-family: 'Roboto';
|
||||
color: #777;
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: .2s;
|
||||
pointer-events: none;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
& i {
|
||||
margin-top: 24px;
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
& img {
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
& .inner {
|
||||
display: flex;
|
||||
width: 60vw;
|
||||
height: 56vh;
|
||||
padding-top: 14px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
height: calc(100vh - 86px);
|
||||
}
|
||||
}
|
||||
204
src/js/pages/Layout.js
Normal file
204
src/js/pages/Layout.js
Normal file
@@ -0,0 +1,204 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { ipcRenderer, remote } from 'electron';
|
||||
|
||||
import classes from './Layout.css';
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import Login from './Login';
|
||||
import UserInfo from './UserInfo';
|
||||
import AddFriend from './AddFriend';
|
||||
import NewChat from './NewChat';
|
||||
import Members from './Members';
|
||||
import AddMember from './AddMember';
|
||||
import BatchSend from './BatchSend';
|
||||
import Forward from './Forward';
|
||||
import ConfirmImagePaste from './ConfirmImagePaste';
|
||||
import Loader from 'components/Loader';
|
||||
import Snackbar from 'components/Snackbar';
|
||||
import Offline from 'components/Offline';
|
||||
|
||||
@inject(stores => ({
|
||||
isLogin: () => !!stores.session.auth,
|
||||
loading: stores.session.loading,
|
||||
message: stores.snackbar.text,
|
||||
show: stores.snackbar.show,
|
||||
process: stores.chat.process,
|
||||
reconnect: stores.session.checkTimeout,
|
||||
close: () => stores.snackbar.toggle(false),
|
||||
canidrag: () => !!stores.chat.user && !stores.batchsend.show,
|
||||
}))
|
||||
@observer
|
||||
export default class Layout extends Component {
|
||||
state = {
|
||||
offline: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
var templates = [
|
||||
{
|
||||
label: 'Undo',
|
||||
role: 'undo',
|
||||
}, {
|
||||
label: 'Redo',
|
||||
role: 'redo',
|
||||
}, {
|
||||
type: 'separator',
|
||||
}, {
|
||||
label: 'Cut',
|
||||
role: 'cut',
|
||||
}, {
|
||||
label: 'Copy',
|
||||
role: 'copy',
|
||||
}, {
|
||||
label: 'Paste',
|
||||
role: 'paste',
|
||||
}, {
|
||||
type: 'separator',
|
||||
}, {
|
||||
label: 'Select all',
|
||||
role: 'selectall',
|
||||
},
|
||||
];
|
||||
var menu = new remote.Menu.buildFromTemplate(templates);
|
||||
var canidrag = this.props.canidrag;
|
||||
|
||||
document.body.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
|
||||
let node = e.target;
|
||||
|
||||
while (node) {
|
||||
if (node.nodeName.match(/^(input|textarea)$/i)
|
||||
|| node.isContentEditable) {
|
||||
menu.popup(remote.getCurrentWindow());
|
||||
break;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.setState({
|
||||
offline: true,
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
// Reconnect to wechat
|
||||
this.props.reconnect();
|
||||
this.setState({
|
||||
offline: false,
|
||||
});
|
||||
});
|
||||
|
||||
if (window.process.platform === 'win32') {
|
||||
document.body.classList.add('isWin');
|
||||
}
|
||||
|
||||
window.ondragover = e => {
|
||||
if (canidrag()) {
|
||||
this.refs.holder.classList.add(classes.show);
|
||||
this.refs.viewport.classList.add(classes.blur);
|
||||
}
|
||||
|
||||
// If not st as 'copy', electron will open the drop file
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
return false;
|
||||
};
|
||||
|
||||
window.ondragleave = () => {
|
||||
if (!canidrag()) return false;
|
||||
|
||||
this.refs.holder.classList.remove(classes.show);
|
||||
this.refs.viewport.classList.remove(classes.blur);
|
||||
};
|
||||
|
||||
window.ondragend = e => {
|
||||
return false;
|
||||
};
|
||||
|
||||
window.ondrop = e => {
|
||||
var files = e.dataTransfer.files;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (files.length && canidrag()) {
|
||||
Array.from(files).map(e => this.props.process(e));
|
||||
}
|
||||
|
||||
this.refs.holder.classList.remove(classes.show);
|
||||
this.refs.viewport.classList.remove(classes.blur);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
var { isLogin, loading, show, close, message, location } = this.props;
|
||||
|
||||
if (!window.navigator.onLine) {
|
||||
return (
|
||||
<Offline show={true} style={{
|
||||
top: 0,
|
||||
paddingTop: 30
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLogin()) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
ipcRenderer.send('logined');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Snackbar
|
||||
close={close}
|
||||
show={show}
|
||||
text={message} />
|
||||
|
||||
<Loader show={loading} />
|
||||
<Header location={location} />
|
||||
<div
|
||||
className={classes.container}
|
||||
ref="viewport">
|
||||
{this.props.children}
|
||||
</div>
|
||||
<Footer
|
||||
location={location}
|
||||
ref="footer" />
|
||||
<UserInfo />
|
||||
<AddFriend />
|
||||
<NewChat />
|
||||
<Members />
|
||||
<BatchSend />
|
||||
<AddMember />
|
||||
<ConfirmImagePaste />
|
||||
<Forward />
|
||||
|
||||
<Offline show={this.state.offline} />;
|
||||
|
||||
<div
|
||||
className={classes.dragDropHolder}
|
||||
ref="holder">
|
||||
<div className={classes.inner}>
|
||||
<div>
|
||||
<img src="assets/images/filetypes/image.png" />
|
||||
<img src="assets/images/filetypes/word.png" />
|
||||
<img src="assets/images/filetypes/pdf.png" />
|
||||
<img src="assets/images/filetypes/archive.png" />
|
||||
<img src="assets/images/filetypes/video.png" />
|
||||
<img src="assets/images/filetypes/audio.png" />
|
||||
</div>
|
||||
|
||||
<i className="icon-ion-ios-cloud-upload-outline" />
|
||||
|
||||
<h2>Drop your file here</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
59
src/js/pages/Login/index.js
Normal file
59
src/js/pages/Login/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
|
||||
@inject(stores => ({
|
||||
avatar: stores.session.avatar,
|
||||
code: stores.session.code,
|
||||
getCode: stores.session.getCode,
|
||||
}))
|
||||
@observer
|
||||
export default class Login extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getCode();
|
||||
}
|
||||
|
||||
renderUser() {
|
||||
return (
|
||||
<div className={classes.inner}>
|
||||
{
|
||||
<img
|
||||
className="disabledDrag"
|
||||
src={this.props.avatar} />
|
||||
}
|
||||
|
||||
<p>Scan successful</p>
|
||||
<p>Confirm login on mobile WeChat</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCode() {
|
||||
var { code } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.inner}>
|
||||
{
|
||||
code && (<img className="disabledDrag" src={`https://login.weixin.qq.com/qrcode/${code}`} />)
|
||||
}
|
||||
|
||||
<a href={window.location.pathname + '?' + +new Date()}>Refresh the QR Code</a>
|
||||
|
||||
<p>Scan to log in to WeChat</p>
|
||||
<p>Log in on phone to use WeChat on Web</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
{
|
||||
this.props.avatar ? this.renderUser() : this.renderCode()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
55
src/js/pages/Login/style.css
Normal file
55
src/js/pages/Login/style.css
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
margin-top: 44px;
|
||||
text-align: center;
|
||||
|
||||
& img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& p {
|
||||
color: #000;
|
||||
font-family: "Helvetica-Light";
|
||||
}
|
||||
|
||||
& p:last-child {
|
||||
margin-top: 20px;
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& a:hover {
|
||||
border-bottom-color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.inner {
|
||||
width: 260px;
|
||||
|
||||
& p:last-child {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/js/pages/Members/index.js
Normal file
119
src/js/pages/Members/index.js
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
@inject(stores => ({
|
||||
show: stores.members.show,
|
||||
close: () => stores.members.toggle(false),
|
||||
user: stores.members.user,
|
||||
list: stores.members.list,
|
||||
search: stores.members.search,
|
||||
searching: stores.members.query,
|
||||
filtered: stores.members.filtered,
|
||||
showUserinfo: async(user) => {
|
||||
var me = stores.session.user.User;
|
||||
var caniremove = helper.isChatRoomOwner(stores.members.user);
|
||||
|
||||
if (user.UserName === me.UserName) {
|
||||
user = me;
|
||||
} else {
|
||||
stores.contacts.memberList.find(e => {
|
||||
// Try to find contact in contacts
|
||||
if (e.UserName === user.UserName) {
|
||||
return (user = e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stores.userinfo.toggle(true, user, caniremove);
|
||||
},
|
||||
addMember: () => {
|
||||
stores.members.toggle(false);
|
||||
stores.addmember.toggle(true);
|
||||
}
|
||||
}))
|
||||
@observer
|
||||
export default class Members extends Component {
|
||||
render() {
|
||||
var { user, searching, list, filtered } = this.props;
|
||||
|
||||
if (!this.props.show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<header>
|
||||
<span dangerouslySetInnerHTML={{ __html: `Group '${user.NickName}' has ${list.length} members` }} />
|
||||
|
||||
<span>
|
||||
<i
|
||||
className="icon-ion-android-add"
|
||||
onClick={e => this.props.addMember()}
|
||||
style={{
|
||||
marginRight: 20,
|
||||
}} />
|
||||
|
||||
<i
|
||||
className="icon-ion-android-close"
|
||||
onClick={e => this.props.close()} />
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<ul className={classes.list}>
|
||||
{
|
||||
(searching && filtered.length === 0) && (
|
||||
<div className={classes.notfound}>
|
||||
<img src="assets/images/crash.png" />
|
||||
<h1>Can't find any people matching '{searching}'</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(searching ? filtered : list).map((e, index) => {
|
||||
var pallet = e.pallet || [];
|
||||
var frontColor = pallet[1] || [0, 0, 0];
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
onClick={ev => this.props.showUserinfo(e)}
|
||||
style={{
|
||||
color: `rgb(
|
||||
${frontColor[0]},
|
||||
${frontColor[1]},
|
||||
${frontColor[2]}
|
||||
)`,
|
||||
}}>
|
||||
<div
|
||||
className={classes.cover}
|
||||
style={{
|
||||
backgroundImage: `url(${e.HeadImgUrl})`,
|
||||
}} />
|
||||
<span
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{ __html: e.NickName }} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div className={classes.footer}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
id="messageInput"
|
||||
maxLength={30}
|
||||
onInput={e => this.props.search(e.target.value)}
|
||||
placeholder="Type something to search..."
|
||||
ref="input"
|
||||
type="text" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
183
src/js/pages/Members/style.css
Normal file
183
src/js/pages/Members/style.css
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
z-index: 9;
|
||||
|
||||
& header {
|
||||
display: flex;
|
||||
padding: 0 12px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 100;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
box-shadow: inset 0 -1px 0 0 #eaedea;
|
||||
}
|
||||
|
||||
& header i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& header span:nth-child(1) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 17px;
|
||||
}
|
||||
|
||||
& header i:hover {
|
||||
color: #34b7f1;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
height: calc(100vh - 90px - 60px);
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
& li {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0 14px;
|
||||
margin: 6px 8px;
|
||||
height: 32px;
|
||||
min-width: 70px;
|
||||
line-height: 32px;
|
||||
border-radius: 1px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .3);
|
||||
overflow: hidden;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
|
||||
& li:hover .cover {
|
||||
filter: opacity(1);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: opacity(.6);
|
||||
transform: scale(1);
|
||||
transition: .2s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, .3);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 60px;
|
||||
box-shadow: inset 0 1px 0 0 #eaedea;
|
||||
|
||||
& input {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
border: 0;
|
||||
background: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notfound {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
& img {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-family: 'Roboto';
|
||||
font-weight: 100;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
& header {
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
height: calc(100vh - 80px - 46px);
|
||||
|
||||
& li {
|
||||
padding: 0 10px;
|
||||
margin: 4px 6px;
|
||||
height: 28px;
|
||||
min-width: 70px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 46px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.notfound {
|
||||
& img {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
word-spacing: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/js/pages/NewChat/index.js
Normal file
126
src/js/pages/NewChat/index.js
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Modal, ModalBody } from 'components/Modal';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
import UserList from 'components/UserList';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
@inject(stores => ({
|
||||
show: stores.newchat.show,
|
||||
searching: stores.newchat.query,
|
||||
getList: () => {
|
||||
var { newchat, contacts } = stores;
|
||||
|
||||
if (newchat.query) {
|
||||
return newchat.list;
|
||||
}
|
||||
|
||||
return contacts.memberList;
|
||||
},
|
||||
getUser: (userid) => {
|
||||
return stores.contacts.memberList.find(e => e.UserName === userid);
|
||||
},
|
||||
search: stores.newchat.search,
|
||||
createChatRoom: stores.newchat.createChatRoom,
|
||||
close: () => {
|
||||
stores.newchat.reset();
|
||||
stores.newchat.toggle(false);
|
||||
},
|
||||
chatTo: (user) => stores.chat.chatTo(user),
|
||||
}))
|
||||
@observer
|
||||
export default class NewChat extends Component {
|
||||
state = {
|
||||
selected: [],
|
||||
};
|
||||
|
||||
async chat() {
|
||||
var selected = this.state.selected;
|
||||
|
||||
if (selected.length === 1) {
|
||||
this.props.chatTo(this.props.getUser(selected[0]));
|
||||
} else {
|
||||
// You can not create a chat room by another chat room
|
||||
let user = await this.props.createChatRoom(selected.filter(e => !helper.isChatRoom(e)));
|
||||
this.props.chatTo(user);
|
||||
}
|
||||
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
document.querySelector('#messageInput').focus();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.props.close();
|
||||
this.setState({
|
||||
selected: [],
|
||||
});
|
||||
}
|
||||
|
||||
renderList() {
|
||||
var self = this;
|
||||
var { show, searching, search, getList } = this.props;
|
||||
|
||||
if (!show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserList {...{
|
||||
ref: 'users',
|
||||
|
||||
search,
|
||||
getList,
|
||||
searching,
|
||||
|
||||
onChange(selected) {
|
||||
self.setState({
|
||||
selected,
|
||||
});
|
||||
}
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
fullscreen={true}
|
||||
onCancel={e => this.props.close()}
|
||||
show={this.props.show}>
|
||||
<ModalBody className={classes.container}>
|
||||
New Chat ({this.state.selected.length} / 20)
|
||||
|
||||
<div className={classes.avatars}>
|
||||
{
|
||||
this.state.selected.map((e, index) => {
|
||||
var user = this.props.getUser(e);
|
||||
return (
|
||||
<img
|
||||
key={index}
|
||||
onClick={ev => this.refs.users.removeSelected(e)}
|
||||
src={user.HeadImgUrl} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{this.renderList()}
|
||||
|
||||
<div>
|
||||
<button
|
||||
disabled={!this.state.selected.length}
|
||||
onClick={e => this.chat()}>
|
||||
Chat
|
||||
</button>
|
||||
|
||||
<button onClick={e => this.close()}>Cancel</button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
104
src/js/pages/NewChat/style.css
Normal file
104
src/js/pages/NewChat/style.css
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
.container {
|
||||
background: #fff;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Roboto';
|
||||
font-size: 36px;
|
||||
color: #000;
|
||||
font-weight: 100;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 4px;
|
||||
|
||||
& input {
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
& button {
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
background-color: rgba(99, 99, 99, 0);
|
||||
font-family: 'Roboto';
|
||||
font-weight: 500;
|
||||
color: rgba(33, 150, 243, .9);
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 1px;
|
||||
text-transform: uppercase;
|
||||
margin: 0 12px;
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& button:disabled {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
& button:hover {
|
||||
background: rgba(99, 99, 99, .2);
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
min-height: 20px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 12px 4px;
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, .3);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
font-size: 26px;
|
||||
letter-spacing: 2px;
|
||||
word-spacing: 2px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
& button {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
letter-spacing: .4px;
|
||||
padding: 0 12px;
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
min-height: 20px;
|
||||
|
||||
& img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 12px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/js/pages/Settings/index.js
Normal file
180
src/js/pages/Settings/index.js
Normal file
@@ -0,0 +1,180 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
|
||||
import classes from './style.css';
|
||||
import Switch from 'components/Switch';
|
||||
import Avatar from 'components/Avatar';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
@inject(stores => ({
|
||||
alwaysOnTop: stores.settings.alwaysOnTop,
|
||||
setAlwaysOnTop: stores.settings.setAlwaysOnTop,
|
||||
showOnTray: stores.settings.showOnTray,
|
||||
setShowOnTray: stores.settings.setShowOnTray,
|
||||
showNotification: stores.settings.showNotification,
|
||||
setShowNotification: stores.settings.setShowNotification,
|
||||
startup: stores.settings.startup,
|
||||
setStartup: stores.settings.setStartup,
|
||||
downloads: stores.settings.downloads,
|
||||
setDownloads: stores.settings.setDownloads,
|
||||
confirmImagePaste: stores.settings.confirmImagePaste,
|
||||
setConfirmImagePaste: stores.settings.setConfirmImagePaste,
|
||||
blockRecall: stores.settings.blockRecall,
|
||||
setBlockRecall: stores.settings.setBlockRecall,
|
||||
rememberConversation: stores.settings.rememberConversation,
|
||||
setRememberConversation: stores.settings.setRememberConversation,
|
||||
showRedIcon: stores.settings.showRedIcon,
|
||||
setShowRedIcon: stores.settings.setShowRedIcon,
|
||||
|
||||
user: stores.session.user,
|
||||
logout: stores.session.logout,
|
||||
}))
|
||||
@observer
|
||||
export default class Settings extends Component {
|
||||
choiceDownloadDir() {
|
||||
this.refs.downloads.click();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.downloads.webkitdirectory = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
var {
|
||||
alwaysOnTop,
|
||||
setAlwaysOnTop,
|
||||
showOnTray,
|
||||
setShowOnTray,
|
||||
showNotification,
|
||||
setShowNotification,
|
||||
startup,
|
||||
setStartup,
|
||||
downloads,
|
||||
setDownloads,
|
||||
confirmImagePaste,
|
||||
setConfirmImagePaste,
|
||||
blockRecall,
|
||||
setBlockRecall,
|
||||
rememberConversation,
|
||||
setRememberConversation,
|
||||
showRedIcon,
|
||||
setShowRedIcon,
|
||||
user,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<div className={classes.column}>
|
||||
<h2>Settings</h2>
|
||||
|
||||
<ul>
|
||||
{
|
||||
user && (
|
||||
<li className={classes.user}>
|
||||
<Avatar src={this.props.user.User.HeadImgUrl} />
|
||||
<button onClick={e => this.props.logout()}>Logout</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
<li className={classes.downloads}>
|
||||
<div>
|
||||
<input
|
||||
onChange={e => setDownloads(e.target.files[0])}
|
||||
ref="downloads"
|
||||
type="file" />
|
||||
<p>Downloads</p>
|
||||
<p onClick={e => this.choiceDownloadDir()}>{downloads}</p>
|
||||
</div>
|
||||
|
||||
<button onClick={e => this.choiceDownloadDir()}>Change</button>
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="alwaysOnTop">
|
||||
<span>Always on Top</span>
|
||||
<Switch
|
||||
checked={alwaysOnTop}
|
||||
id="alwaysOnTop"
|
||||
onChange={e => setAlwaysOnTop(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="showOnTray">
|
||||
<span>Show on Tray</span>
|
||||
<Switch
|
||||
checked={showOnTray}
|
||||
disabled={!helper.isOsx}
|
||||
id="showOnTray"
|
||||
onChange={e => setShowOnTray(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="showNotification">
|
||||
<span>Send Desktop Notifications</span>
|
||||
<Switch
|
||||
checked={showNotification}
|
||||
id="showNotification"
|
||||
onChange={e => setShowNotification(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="blockRecall">
|
||||
<span>Block Message Recall</span>
|
||||
<Switch
|
||||
checked={blockRecall}
|
||||
id="blockRecall"
|
||||
onChange={e => setBlockRecall(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="rememberConversation">
|
||||
<span>Remember the last Conversation</span>
|
||||
<Switch
|
||||
checked={rememberConversation}
|
||||
id="rememberConversation"
|
||||
onChange={e => setRememberConversation(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="showRedIcon">
|
||||
<span>Show the red button</span>
|
||||
<Switch
|
||||
checked={showRedIcon}
|
||||
id="showRedIcon"
|
||||
onChange={e => setShowRedIcon(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="confirmImagePaste">
|
||||
<span>Image paste Confirmation</span>
|
||||
<Switch
|
||||
checked={confirmImagePaste}
|
||||
id="confirmImagePaste"
|
||||
onChange={e => setConfirmImagePaste(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="startup">
|
||||
<span>Launch at startup</span>
|
||||
<Switch
|
||||
checked={startup}
|
||||
id="startup"
|
||||
onChange={e => setStartup(e.target.checked)} />
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={classes.column}>
|
||||
<h2>TODO:</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
145
src/js/pages/Settings/style.css
Normal file
145
src/js/pages/Settings/style.css
Normal file
@@ -0,0 +1,145 @@
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 17px 0;
|
||||
font-family: 'Helvetica Neue';
|
||||
color: #000;
|
||||
|
||||
& h2 {
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 50%;
|
||||
|
||||
& ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: 42px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
& li {
|
||||
padding-right: 42px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 13px;
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
& label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& button {
|
||||
padding: 8px 12px;
|
||||
text-transform: uppercase;
|
||||
border: 0;
|
||||
border-radius: 1px;
|
||||
background: none;
|
||||
color: rgb(117, 117, 117);
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
& button:hover {
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.user,
|
||||
.downloads {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user img {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.downloads {
|
||||
& p {
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
& p:last-child {
|
||||
color: rgb(117, 117, 117);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& img {
|
||||
display: block;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
margin-top: 12px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
/* stylelint-disable */
|
||||
& :global(.Switch) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
/* stylelint-enable */
|
||||
|
||||
& :any-link {
|
||||
color: #4990e2;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
display: inline-block;
|
||||
margin-left: 24px;
|
||||
font-size: 12px;
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.description {
|
||||
font: 12px;
|
||||
color: #9b9b9b;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
padding: 8px 17px 0;
|
||||
|
||||
& h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
& ul {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
& li {
|
||||
padding-right: 32px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
236
src/js/pages/UserInfo/index.js
Normal file
236
src/js/pages/UserInfo/index.js
Normal file
@@ -0,0 +1,236 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import pinyin from 'han';
|
||||
import clazz from 'classname';
|
||||
|
||||
import classes from './style.css';
|
||||
import Avatar from 'components/Avatar';
|
||||
import { Modal, ModalBody } from 'components/Modal';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
@inject(stores => ({
|
||||
chatTo: (userid) => {
|
||||
var user = stores.contacts.memberList.find(e => e.UserName === userid);
|
||||
stores.chat.chatTo(user);
|
||||
},
|
||||
pallet: stores.userinfo.pallet,
|
||||
show: stores.userinfo.show,
|
||||
user: stores.userinfo.user,
|
||||
remove: stores.userinfo.remove,
|
||||
toggle: stores.userinfo.toggle,
|
||||
setRemarkName: stores.userinfo.setRemarkName,
|
||||
removeMember: async(user) => {
|
||||
var roomid = (stores.members.show && stores.members.user.UserName)
|
||||
|| stores.chat.user.UserName;
|
||||
|
||||
await stores.userinfo.removeMember(roomid, user.UserName);
|
||||
stores.userinfo.toggle(false);
|
||||
},
|
||||
refreshContacts: async(user) => {
|
||||
var { updateUser, filter, filtered } = stores.contacts;
|
||||
|
||||
stores.userinfo.updateUser(user);
|
||||
updateUser(user);
|
||||
filter(filtered.query);
|
||||
},
|
||||
showAddFriend: (user) => stores.addfriend.toggle(true, user),
|
||||
showMessage: stores.snackbar.showMessage,
|
||||
isme: () => {
|
||||
return stores.session.user
|
||||
&& stores.userinfo.user.UserName === stores.session.user.User.UserName;
|
||||
},
|
||||
}))
|
||||
@observer
|
||||
class UserInfo extends Component {
|
||||
state = {
|
||||
showEdit: false,
|
||||
};
|
||||
|
||||
toggleEdit(showEdit = !this.state.showEdit) {
|
||||
this.setState({ showEdit });
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.toggleEdit(false);
|
||||
this.props.toggle(false);
|
||||
}
|
||||
|
||||
handleError(e) {
|
||||
e.target.src = 'http://i.pravatar.cc/200';
|
||||
}
|
||||
|
||||
async handleEnter(e) {
|
||||
if (e.charCode !== 13) return;
|
||||
|
||||
var value = e.target.value.trim();
|
||||
var res = await this.props.setRemarkName(value, this.props.user.UserName);
|
||||
|
||||
if (res) {
|
||||
this.props.refreshContacts({
|
||||
...this.props.user,
|
||||
RemarkName: value,
|
||||
RemarkPYInitial: value ? (pinyin.letter(value)[0]).toUpperCase() : value,
|
||||
});
|
||||
this.toggleEdit(false);
|
||||
} else {
|
||||
this.props.showMessage('Failed to set remark name.');
|
||||
}
|
||||
}
|
||||
|
||||
handleAction(user) {
|
||||
if (this.props.history.location.pathname !== '/') {
|
||||
this.props.history.push('/');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (helper.isContact(user) || helper.isChatRoom(user.UserName)) {
|
||||
this.props.toggle(false);
|
||||
this.props.chatTo(user.UserName);
|
||||
document.querySelector('#messageInput').focus();
|
||||
} else {
|
||||
this.props.showAddFriend(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var { UserName, HeadImgUrl, NickName, RemarkName, Signature, City, Province } = this.props.user;
|
||||
var isFriend = helper.isContact(this.props.user);
|
||||
var pallet = this.props.pallet;
|
||||
var isme = this.props.isme();
|
||||
var background = pallet[0];
|
||||
var gradient = 'none';
|
||||
var fontColor = '#777';
|
||||
var buttonColor = '#777';
|
||||
|
||||
if (background) {
|
||||
let pallet4font = pallet[1] || [0, 0, 0];
|
||||
let pallet4button = pallet[2] || [0, 0, 0];
|
||||
|
||||
gradient = `
|
||||
-webkit-linear-gradient(top, rgb(${background[0]}, ${background[1]}, ${background[2]}) 5%, rgba(${background[0]}, ${background[1]}, ${background[2]}, 0) 15%),
|
||||
-webkit-linear-gradient(bottom, rgb(${background[0]}, ${background[1]}, ${background[2]}) 5%, rgba(${background[0]}, ${background[1]}, ${background[2]}, 0) 15%),
|
||||
-webkit-linear-gradient(left, rgb(${background[0]}, ${background[1]}, ${background[2]}) 5%, rgba(${background[0]}, ${background[1]}, ${background[2]}, 0) 15%),
|
||||
-webkit-linear-gradient(right, rgb(${background[0]}, ${background[1]}, ${background[2]}) 5%, rgba(${background[0]}, ${background[1]}, ${background[2]}, 0) 15%)
|
||||
`;
|
||||
background = `rgba(${background[0]}, ${background[1]}, ${background[2]}, 1)`;
|
||||
fontColor = `rgb(
|
||||
${pallet4font[0]},
|
||||
${pallet4font[1]},
|
||||
${pallet4font[2]},
|
||||
)`;
|
||||
buttonColor = `rgb(
|
||||
${pallet4button[0]},
|
||||
${pallet4button[1]},
|
||||
${pallet4button[2]},
|
||||
)`;
|
||||
} else {
|
||||
background = '#fff';
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onCancel={() => this.handleClose()}
|
||||
show={this.props.show}>
|
||||
<ModalBody className={classes.container}>
|
||||
<div
|
||||
className={clazz(classes.hero, {
|
||||
[classes.showEdit]: this.state.showEdit,
|
||||
[classes.large]: !this.props.remove,
|
||||
[classes.isme]: isme,
|
||||
})}
|
||||
onClick={() => {
|
||||
var showEdit = this.state.showEdit;
|
||||
|
||||
if (showEdit) {
|
||||
this.toggleEdit();
|
||||
}
|
||||
}} style={{
|
||||
background,
|
||||
color: fontColor,
|
||||
}}>
|
||||
|
||||
{
|
||||
(!isme && isFriend) && (
|
||||
<div
|
||||
className={classes.edit}
|
||||
onClick={() => this.toggleEdit()}>
|
||||
<i className="icon-ion-edit" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={classes.inner}>
|
||||
<div
|
||||
className={classes.mask}
|
||||
style={{
|
||||
background: gradient
|
||||
}} />
|
||||
<Avatar src={HeadImgUrl} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classes.username}
|
||||
dangerouslySetInnerHTML={{__html: NickName}} />
|
||||
|
||||
{
|
||||
!this.props.remove ? (
|
||||
<div className={classes.wrap}>
|
||||
<p dangerouslySetInnerHTML={{__html: Signature || 'No Signature'}} />
|
||||
|
||||
<div className={classes.address}>
|
||||
<i
|
||||
className="icon-ion-android-map"
|
||||
style={{ color: fontColor }} />
|
||||
|
||||
{City || 'UNKNOW'}, {Province || 'UNKNOW'}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classes.action}
|
||||
onClick={() => this.props.removeMember(this.props.user)}
|
||||
style={{
|
||||
color: buttonColor,
|
||||
opacity: .6,
|
||||
marginTop: 20,
|
||||
marginBottom: -30,
|
||||
}}>
|
||||
Delete
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
className={classes.action}
|
||||
onClick={() => this.handleAction(this.props.user)}
|
||||
style={{
|
||||
color: buttonColor,
|
||||
opacity: .6,
|
||||
}}>
|
||||
{helper.isChatRoom(UserName) || isFriend ? 'Send Message' : 'Add Friend'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* eslint-disable */
|
||||
this.state.showEdit && (
|
||||
<input
|
||||
autoFocus={true}
|
||||
defaultValue={RemarkName}
|
||||
onKeyPress={e => this.handleEnter(e)}
|
||||
placeholder="Type the remark name"
|
||||
ref="input"
|
||||
type="text" />
|
||||
)
|
||||
/* eslint-enable */
|
||||
}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(UserInfo);
|
||||
241
src/js/pages/UserInfo/style.css
Normal file
241
src/js/pages/UserInfo/style.css
Normal file
@@ -0,0 +1,241 @@
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
width: 320px;
|
||||
overflow: hidden;
|
||||
|
||||
& input {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 40px;
|
||||
text-align: center;
|
||||
background: #e1306c;
|
||||
box-shadow: 0 0 24px 0 rgba(119, 119, 119, .6);
|
||||
cursor: pointer;
|
||||
|
||||
& i {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
height: 370px;
|
||||
padding-top: 20px;
|
||||
font-family: 'Helvetica';
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
transition: .2s ease-in-out;
|
||||
|
||||
&.large {
|
||||
height: 400px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
& .username {
|
||||
display: -webkit-box;
|
||||
padding: 0 12px;
|
||||
line-height: 16px; /* fallback */
|
||||
max-height: 32px; /* fallback */
|
||||
-webkit-line-clamp: 2; /* number of lines to show */
|
||||
-webkit-box-orient: vertical;
|
||||
letter-spacing: 1px;
|
||||
color: #4a4a4a;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
& .wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& p {
|
||||
display: -webkit-box;
|
||||
line-height: 16px;
|
||||
max-height: 32px;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
padding: 0 12px;
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.hero.showEdit {
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
|
||||
.hero.isme {
|
||||
height: 350px;
|
||||
|
||||
& .address {
|
||||
border: 0;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
& .action {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.address {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-family: 'Roboto';
|
||||
text-align: center;
|
||||
border-bottom: thin solid;
|
||||
opacity: .8;
|
||||
|
||||
& i {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
|
||||
& img {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.action {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-top: 44px;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Roboto';
|
||||
font-size: 24px;
|
||||
font-weight: 200;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 800px) {
|
||||
.container {
|
||||
width: 240px;
|
||||
|
||||
& input {
|
||||
height: 46px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
height: 280px;
|
||||
padding-top: 10px;
|
||||
|
||||
&.large {
|
||||
height: 300px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
letter-spacing: 1px;
|
||||
color: #4a4a4a;
|
||||
}
|
||||
|
||||
& p {
|
||||
padding: 0 8px;
|
||||
max-width: 280px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero.showEdit {
|
||||
transform: translateY(-46px);
|
||||
}
|
||||
|
||||
.hero.isme {
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.address {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
line-height: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
& img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.mask {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 33px;
|
||||
padding: 6px 10px;
|
||||
font-size: 18px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
7
src/js/pages/index.js
Normal file
7
src/js/pages/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
import Layout from './Layout';
|
||||
import Home from './Home';
|
||||
import Contacts from './Contacts';
|
||||
import Settings from './Settings';
|
||||
|
||||
export { Layout, Home, Contacts, Settings };
|
||||
21
src/js/routes.js
Normal file
21
src/js/routes.js
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import React from 'react';
|
||||
import { withRouter, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { Layout, Settings, Contacts, Home } from './pages';
|
||||
|
||||
const Main = withRouter(props => <Layout {...props} />);
|
||||
|
||||
export default () => {
|
||||
/* eslint-disable */
|
||||
return (
|
||||
<Main>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/contacts" component={Contacts} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
</Switch>
|
||||
</Main>
|
||||
);
|
||||
/* eslint-enable */
|
||||
};
|
||||
41
src/js/stores/addfriend.js
Normal file
41
src/js/stores/addfriend.js
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import axios from 'axios';
|
||||
|
||||
import storage from 'utils/storage';
|
||||
|
||||
class AddFriend {
|
||||
@observable show = false;
|
||||
user;
|
||||
|
||||
@action toggle(show = self.show, user = self.user) {
|
||||
self.show = show;
|
||||
self.user = user;
|
||||
}
|
||||
|
||||
@action async sendRequest(message) {
|
||||
var auth = await storage.get('auth');
|
||||
var response = await axios.post(`/cgi-bin/mmwebwx-bin/webwxverifyuser?r=${+new Date()}`, {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
Opcode: 2,
|
||||
SceneList: [33],
|
||||
SceneListCount: 1,
|
||||
VerifyContent: message,
|
||||
VerifyUserList: [{
|
||||
Value: self.user.UserName,
|
||||
VerifyUserTicket: '',
|
||||
}],
|
||||
VerifyUserListSize: 1,
|
||||
skey: auth.skey,
|
||||
});
|
||||
|
||||
return +response.data.BaseResponse.Ret === 0;
|
||||
}
|
||||
}
|
||||
|
||||
const self = new AddFriend();
|
||||
export default self;
|
||||
66
src/js/stores/addmember.js
Normal file
66
src/js/stores/addmember.js
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import axios from 'axios';
|
||||
import pinyin from 'han';
|
||||
|
||||
import contacts from './contacts';
|
||||
import session from './session';
|
||||
import storage from 'utils/storage';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
class AddMember {
|
||||
@observable show = false;
|
||||
@observable query = '';
|
||||
@observable list = [];
|
||||
|
||||
@action toggle(show = !self.show) {
|
||||
self.show = show;
|
||||
}
|
||||
|
||||
@action search(text) {
|
||||
text = pinyin.letter(text.toLocaleLowerCase());
|
||||
|
||||
var list = contacts.memberList.filter(e => {
|
||||
var res = pinyin.letter(e.NickName).toLowerCase().indexOf(text) > -1;
|
||||
|
||||
if (e.UserName === session.user.User.UserName
|
||||
|| !helper.isContact(e)
|
||||
|| helper.isChatRoom(e.UserName)
|
||||
|| helper.isFileHelper(e)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.RemarkName) {
|
||||
res = res || pinyin.letter(e.RemarkName).toLowerCase().indexOf(text) > -1;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
self.query = text;
|
||||
self.list.replace(list);
|
||||
}
|
||||
|
||||
@action reset() {
|
||||
self.query = '';
|
||||
self.list.replace([]);
|
||||
}
|
||||
|
||||
@action async addMember(roomId, userids) {
|
||||
var auth = await storage.get('auth');
|
||||
var response = await axios.post('/cgi-bin/mmwebwx-bin/webwxupdatechatroom?fun=addmember', {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
ChatRoomName: roomId,
|
||||
AddMemberList: userids.join(','),
|
||||
});
|
||||
|
||||
return +response.data.BaseResponse.Ret === 0;
|
||||
}
|
||||
}
|
||||
|
||||
const self = new AddMember();
|
||||
export default self;
|
||||
48
src/js/stores/batchsend.js
Normal file
48
src/js/stores/batchsend.js
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import pinyin from 'han';
|
||||
|
||||
import contacts from './contacts';
|
||||
|
||||
class BatchSend {
|
||||
@observable show = false;
|
||||
@observable query = '';
|
||||
@observable filtered = [];
|
||||
|
||||
@action async toggle(show = !self.show) {
|
||||
self.show = show;
|
||||
|
||||
if (show === false) {
|
||||
self.query = '';
|
||||
self.filtered.replace([]);
|
||||
}
|
||||
}
|
||||
|
||||
@action search(text = '') {
|
||||
var list = contacts.memberList;
|
||||
|
||||
self.query = text;
|
||||
|
||||
if (text) {
|
||||
text = pinyin.letter(text.toLocaleLowerCase());
|
||||
|
||||
list = list.filter(e => {
|
||||
var res = pinyin.letter(e.NickName).toLowerCase().indexOf(text) > -1;
|
||||
|
||||
if (e.RemarkName) {
|
||||
res = res || pinyin.letter(e.RemarkName).toLowerCase().indexOf(text) > -1;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
self.filtered.replace(list);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self.filtered.replace([]);
|
||||
}
|
||||
}
|
||||
|
||||
const self = new BatchSend();
|
||||
export default self;
|
||||
1092
src/js/stores/chat.js
Normal file
1092
src/js/stores/chat.js
Normal file
File diff suppressed because it is too large
Load Diff
25
src/js/stores/confirmImagePaste.js
Normal file
25
src/js/stores/confirmImagePaste.js
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
|
||||
class ConfirmImagePaste {
|
||||
@observable show = false;
|
||||
@observable image;
|
||||
|
||||
ok;
|
||||
cancel;
|
||||
|
||||
@action toggle(show = self.show, image = self.image) {
|
||||
var promise = new Promise((resolve, reject) => {
|
||||
self.ok = () => resolve(true);
|
||||
self.cancel = () => resolve(false);
|
||||
});
|
||||
|
||||
self.show = show;
|
||||
self.image = image;
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
const self = new ConfirmImagePaste();
|
||||
export default self;
|
||||
253
src/js/stores/contacts.js
Normal file
253
src/js/stores/contacts.js
Normal file
@@ -0,0 +1,253 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import axios from 'axios';
|
||||
import pinyin from 'han';
|
||||
|
||||
import session from './session';
|
||||
import chat from './chat';
|
||||
import storage from 'utils/storage';
|
||||
import helper from 'utils/helper';
|
||||
import { normalize } from 'utils/emoji';
|
||||
|
||||
class Contacts {
|
||||
@observable loading = false;
|
||||
@observable showGroup = true;
|
||||
@observable memberList = [];
|
||||
@observable filtered = {
|
||||
query: '',
|
||||
result: [],
|
||||
};
|
||||
|
||||
@action group(list, showall = false) {
|
||||
var mappings = {};
|
||||
var sorted = [];
|
||||
|
||||
list.map(e => {
|
||||
if (!e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If 'showall' is false, just show your friends
|
||||
if (showall === false
|
||||
&& !helper.isContact(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = ((e.RemarkPYInitial || e.PYInitial || pinyin.letter(e.NickName)).toString()[0] + '').replace('?', '#');
|
||||
var group = mappings[prefix];
|
||||
|
||||
if (!group) {
|
||||
group = mappings[prefix] = [];
|
||||
}
|
||||
group.push(e);
|
||||
});
|
||||
|
||||
for (let key in mappings) {
|
||||
sorted.push({
|
||||
prefix: key,
|
||||
list: mappings[key],
|
||||
});
|
||||
}
|
||||
|
||||
sorted.sort((a, b) => a.prefix.charCodeAt() - b.prefix.charCodeAt());
|
||||
return sorted;
|
||||
}
|
||||
|
||||
@action async getUser(userid) {
|
||||
var user = self.memberList.find(e => e.UserName === userid);
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
await self.batch([userid]);
|
||||
user = await self.getUser(userid);
|
||||
return user;
|
||||
}
|
||||
|
||||
@action async getContats() {
|
||||
self.loading = true;
|
||||
|
||||
var auth = await storage.get('auth');
|
||||
var me = session.user.User;
|
||||
var response = await axios.get('/cgi-bin/mmwebwx-bin/webwxgetcontact', {
|
||||
params: {
|
||||
r: +new Date(),
|
||||
seq: 0,
|
||||
skey: auth.skey
|
||||
}
|
||||
});
|
||||
|
||||
// Remove all official account and brand account
|
||||
self.memberList = response.data.MemberList.filter(e => helper.isContact(e) && !helper.isOfficial(e) && !helper.isBrand(e)).concat(me);
|
||||
self.memberList.map(e => {
|
||||
e.MemberList = [];
|
||||
return self.resolveUser(auth, e);
|
||||
});
|
||||
|
||||
self.loading = false;
|
||||
self.filtered.result = self.group(self.memberList);
|
||||
|
||||
return (window.list = self.memberList);
|
||||
}
|
||||
|
||||
resolveUser(auth, user) {
|
||||
if (helper.isOfficial(user)
|
||||
&& !helper.isFileHelper(user)) {
|
||||
// Skip the official account
|
||||
return;
|
||||
}
|
||||
|
||||
if (helper.isBrand(user)
|
||||
&& !helper.isFileHelper(user)) {
|
||||
// Skip the brand account, eg: JD.COM
|
||||
return;
|
||||
}
|
||||
|
||||
if (helper.isChatRoomRemoved(user)
|
||||
&& !helper.isFileHelper(user)) {
|
||||
// Chat room has removed
|
||||
return;
|
||||
}
|
||||
|
||||
if (helper.isChatRoom(user.UserName)) {
|
||||
let placeholder = user.MemberList.map(e => e.NickName).join(',');
|
||||
|
||||
if (user.NickName) {
|
||||
user.Signature = placeholder;
|
||||
} else {
|
||||
user.NickName = placeholder;
|
||||
user.Signature = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
user.NickName = normalize(user.NickName);
|
||||
user.RemarkName = normalize(user.RemarkName);
|
||||
user.Signature = normalize(user.Signature);
|
||||
|
||||
user.HeadImgUrl = `${axios.defaults.baseURL}${user.HeadImgUrl.substr(1)}`;
|
||||
user.MemberList.map(e => {
|
||||
e.NickName = normalize(e.NickName);
|
||||
e.RemarkName = normalize(e.RemarkName);
|
||||
e.HeadImgUrl = `${axios.defaults.baseURL}cgi-bin/mmwebwx-bin/webwxgeticon?username=${e.UserName}&chatroomid=${user.EncryChatRoomId}&skey=${auth.skey}&seq=0`;
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// Batch get the contacts
|
||||
async batch(list) {
|
||||
var auth = await storage.get('auth');
|
||||
var response = await axios.post(`/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=${+new Date()}`, {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
Count: list.length,
|
||||
List: list.map(e => ({
|
||||
UserName: e,
|
||||
ChatRoomId: ''
|
||||
})),
|
||||
});
|
||||
|
||||
if (response.data.BaseResponse.Ret === 0) {
|
||||
var shouldUpdate = false;
|
||||
|
||||
response.data.ContactList.map(e => {
|
||||
var index = self.memberList.findIndex(user => user.UserName === e.UserName);
|
||||
var user = self.resolveUser(auth, e);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
shouldUpdate = true;
|
||||
|
||||
if (index !== -1) {
|
||||
self.memberList[index] = user;
|
||||
} else {
|
||||
// This contact is not in your contact list, eg: Temprary chat room
|
||||
self.memberList.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldUpdate) {
|
||||
// Update contact in menu
|
||||
ipcRenderer.send('menu-update', {
|
||||
contacts: JSON.stringify(self.memberList.filter(e => helper.isContact(e))),
|
||||
cookies: await helper.getCookie(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to get user: ${list}`);
|
||||
}
|
||||
|
||||
return response.data.ContactList;
|
||||
}
|
||||
|
||||
@action filter(text = '', showall = false) {
|
||||
text = pinyin.letter(text.toLocaleLowerCase());
|
||||
var list = self.memberList.filter(e => {
|
||||
var res = pinyin.letter(e.NickName).toLowerCase().indexOf(text) > -1;
|
||||
|
||||
if (e.RemarkName) {
|
||||
res = res || pinyin.letter(e.RemarkName).toLowerCase().indexOf(text) > -1;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
if (!self.showGroup) {
|
||||
list = list.filter(e => {
|
||||
return !(e.ContactFlag === 3 && e.SnsFlag === 0);
|
||||
});
|
||||
}
|
||||
|
||||
self.filtered = {
|
||||
query: text,
|
||||
result: list.length ? self.group(list, showall) : [],
|
||||
};
|
||||
}
|
||||
|
||||
@action toggleGroup(showGroup) {
|
||||
self.showGroup = showGroup;
|
||||
}
|
||||
|
||||
@action async deleteUser(id) {
|
||||
self.memberList = self.memberList.filter(e => e.UserName !== id);
|
||||
|
||||
// Update contact in menu
|
||||
ipcRenderer.send('menu-update', {
|
||||
contacts: JSON.stringify(self.memberList.filter(e => helper.isContact(e))),
|
||||
cookies: await helper.getCookie(),
|
||||
});
|
||||
}
|
||||
|
||||
@action async updateUser(user) {
|
||||
var auth = await storage.get('auth');
|
||||
var list = self.memberList;
|
||||
var index = list.findIndex(e => e.UserName === user.UserName);
|
||||
var chating = chat.user;
|
||||
|
||||
// Fix chat room miss user avatar
|
||||
user.EncryChatRoomId = list[index]['EncryChatRoomId'];
|
||||
|
||||
user = self.resolveUser(auth, user);
|
||||
|
||||
// Prevent avatar cache
|
||||
user.HeadImgUrl = user.HeadImgUrl.replace(/\?\d{13}$/, '') + `?${+new Date()}`;
|
||||
|
||||
if (index !== -1) {
|
||||
if (chating
|
||||
&& user.UserName === chating.UserName) {
|
||||
Object.assign(chating, user);
|
||||
}
|
||||
|
||||
list[index] = user;
|
||||
self.memberList.replace(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Contacts();
|
||||
export default self;
|
||||
60
src/js/stores/forward.js
Normal file
60
src/js/stores/forward.js
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import pinyin from 'han';
|
||||
|
||||
import contacts from './contacts';
|
||||
import session from './session';
|
||||
import chat from './chat';
|
||||
|
||||
class Forward {
|
||||
@observable show = false;
|
||||
@observable message = {};
|
||||
@observable list = [];
|
||||
@observable query = '';
|
||||
|
||||
@action async toggle(show = self.show, message = {}) {
|
||||
self.show = show;
|
||||
self.message = message;
|
||||
|
||||
if (show === false) {
|
||||
self.query = '';
|
||||
self.list.replace([]);
|
||||
}
|
||||
}
|
||||
|
||||
@action search(text = '') {
|
||||
var list;
|
||||
|
||||
self.query = text;
|
||||
|
||||
if (text) {
|
||||
list = contacts.memberList.filter(e => {
|
||||
if (e.UserName === session.user.User.UserName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pinyin.letter(e.NickName).toLowerCase().indexOf(pinyin.letter(text.toLocaleLowerCase())) > -1;
|
||||
});
|
||||
self.list.replace(list);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self.list.replace([]);
|
||||
}
|
||||
|
||||
@action async send(userid) {
|
||||
var message = self.message;
|
||||
var user = await contacts.getUser(userid);
|
||||
|
||||
message = Object.assign(message, {
|
||||
content: message.Content,
|
||||
type: message.MsgType,
|
||||
});
|
||||
|
||||
chat.sendMessage(user, message, true);
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Forward();
|
||||
export default self;
|
||||
34
src/js/stores/index.js
Normal file
34
src/js/stores/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import session from './session';
|
||||
import chat from './chat';
|
||||
import addfriend from './addfriend';
|
||||
import addmember from './addmember';
|
||||
import members from './members';
|
||||
import newchat from './newchat';
|
||||
import forward from './forward';
|
||||
import userinfo from './userinfo';
|
||||
import contacts from './contacts';
|
||||
import search from './search';
|
||||
import batchsend from './batchsend';
|
||||
import settings from './settings';
|
||||
import snackbar from './snackbar';
|
||||
import confirmImagePaste from './confirmImagePaste';
|
||||
|
||||
const stores = {
|
||||
session,
|
||||
chat,
|
||||
addfriend,
|
||||
addmember,
|
||||
newchat,
|
||||
userinfo,
|
||||
contacts,
|
||||
search,
|
||||
batchsend,
|
||||
settings,
|
||||
members,
|
||||
forward,
|
||||
snackbar,
|
||||
confirmImagePaste,
|
||||
};
|
||||
|
||||
export default stores;
|
||||
63
src/js/stores/members.js
Normal file
63
src/js/stores/members.js
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import pinyin from 'han';
|
||||
|
||||
import helper from 'utils/helper';
|
||||
|
||||
class Members {
|
||||
@observable show = false;
|
||||
@observable user = {
|
||||
MemberList: [],
|
||||
};
|
||||
@observable list = [];
|
||||
@observable filtered = [];
|
||||
@observable query = '';
|
||||
|
||||
@action async toggle(show = self.show, user = self.user) {
|
||||
var list = [];
|
||||
|
||||
self.show = show;
|
||||
self.user = user;
|
||||
|
||||
if (show === false) {
|
||||
self.query = '';
|
||||
self.filtered.replace([]);
|
||||
return;
|
||||
}
|
||||
|
||||
self.list.replace(user.MemberList);
|
||||
|
||||
Promise.all(
|
||||
user.MemberList.map(async e => {
|
||||
var pallet = e.pallet;
|
||||
|
||||
if (!pallet) {
|
||||
e.pallet = await helper.getPallet(e.HeadImgUrl);
|
||||
}
|
||||
list.push(e);
|
||||
})
|
||||
).then(() => {
|
||||
self.list.replace(list);
|
||||
});
|
||||
}
|
||||
|
||||
@action search(text = '') {
|
||||
var list;
|
||||
|
||||
self.query = text;
|
||||
|
||||
if (text) {
|
||||
list = self.list.filter(e => {
|
||||
return pinyin.letter(e.NickName).toLowerCase().indexOf(pinyin.letter(text.toLocaleLowerCase())) > -1;
|
||||
});
|
||||
self.filtered.replace(list);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self.filtered.replace([]);
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Members();
|
||||
export default self;
|
||||
63
src/js/stores/newchat.js
Normal file
63
src/js/stores/newchat.js
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import axios from 'axios';
|
||||
import pinyin from 'han';
|
||||
|
||||
import contacts from './contacts';
|
||||
import storage from 'utils/storage';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
class NewChat {
|
||||
@observable show = false;
|
||||
@observable query = '';
|
||||
@observable list = [];
|
||||
|
||||
@action toggle(show = !self.show) {
|
||||
self.show = show;
|
||||
}
|
||||
|
||||
@action search(text) {
|
||||
text = pinyin.letter(text.toLocaleLowerCase());
|
||||
var list = contacts.memberList.filter(e => {
|
||||
var res = pinyin.letter(e.NickName).toLowerCase().indexOf(text) > -1;
|
||||
|
||||
if (e.RemarkName) {
|
||||
res = res || pinyin.letter(e.RemarkName).toLowerCase().indexOf(text) > -1;
|
||||
}
|
||||
|
||||
return helper.isContact(e) && res;
|
||||
});
|
||||
|
||||
self.query = text;
|
||||
self.list.replace(list);
|
||||
}
|
||||
|
||||
@action reset() {
|
||||
self.query = '';
|
||||
self.list.replace([]);
|
||||
}
|
||||
|
||||
@action async createChatRoom(userids) {
|
||||
var auth = await storage.get('auth');
|
||||
var response = await axios.post(`/cgi-bin/mmwebwx-bin/webwxcreatechatroom?r=${+new Date()}`, {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
MemberCount: userids.length,
|
||||
MemberList: userids.map(e => ({ UserName: e }))
|
||||
});
|
||||
|
||||
if (+response.data.BaseResponse.Ret === 0) {
|
||||
// Load the new contact infomation
|
||||
let user = await contacts.getUser(response.data.ChatRoomName);
|
||||
return user;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const self = new NewChat();
|
||||
export default self;
|
||||
106
src/js/stores/search.js
Normal file
106
src/js/stores/search.js
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import pinyin from 'han';
|
||||
|
||||
import contacts from './contacts';
|
||||
import storage from 'utils/storage';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
class Search {
|
||||
@observable history = [];
|
||||
@observable result = {
|
||||
query: '',
|
||||
friend: [],
|
||||
groups: [],
|
||||
};
|
||||
@observable searching = false;
|
||||
|
||||
@action filter(text = '') {
|
||||
var list = contacts.memberList;
|
||||
var groups = [];
|
||||
var friend = [];
|
||||
|
||||
text = pinyin.letter(text.toLocaleLowerCase());
|
||||
|
||||
list = contacts.memberList.filter(e => {
|
||||
var res = pinyin.letter(e.NickName).toLowerCase().indexOf(text) > -1;
|
||||
|
||||
if (e.RemarkName) {
|
||||
res = res || pinyin.letter(e.RemarkName).toLowerCase().indexOf(text) > -1;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
list.map(e => {
|
||||
if (helper.isChatRoom(e.UserName)) {
|
||||
return groups.push(e);
|
||||
}
|
||||
|
||||
friend.push(e);
|
||||
});
|
||||
|
||||
if (text) {
|
||||
self.result = {
|
||||
query: text,
|
||||
friend,
|
||||
groups,
|
||||
};
|
||||
} else {
|
||||
self.result = {
|
||||
query: text,
|
||||
friend: [],
|
||||
groups: [],
|
||||
};
|
||||
}
|
||||
|
||||
self.searching = true;
|
||||
return self.result;
|
||||
}
|
||||
|
||||
@action clearHistory() {
|
||||
self.history = [];
|
||||
storage.remove('history', []);
|
||||
}
|
||||
|
||||
@action async addHistory(user) {
|
||||
var list = [user, ...self.history.filter(e => e.UserName !== user.UserName)];
|
||||
|
||||
await storage.set('history', list);
|
||||
await self.getHistory();
|
||||
}
|
||||
|
||||
@action reset() {
|
||||
self.result = {
|
||||
query: '',
|
||||
friend: [],
|
||||
groups: [],
|
||||
};
|
||||
self.toggle(false);
|
||||
}
|
||||
|
||||
@action async getHistory() {
|
||||
var list = await storage.get('history');
|
||||
var history = [];
|
||||
|
||||
Array.from(list).map(e => {
|
||||
var user = contacts.memberList.find(user => user.UserName === e.UserName);
|
||||
|
||||
if (user) {
|
||||
history.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
await storage.set('history', history);
|
||||
self.history.replace(history);
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
@action toggle(searching = !self.searching) {
|
||||
self.searching = searching;
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Search();
|
||||
export default self;
|
||||
347
src/js/stores/session.js
Normal file
347
src/js/stores/session.js
Normal file
@@ -0,0 +1,347 @@
|
||||
|
||||
/* eslint-disable no-eval */
|
||||
import axios from 'axios';
|
||||
import { observable, action } from 'mobx';
|
||||
|
||||
import helper from 'utils/helper';
|
||||
import storage from 'utils/storage';
|
||||
import { normalize } from 'utils/emoji';
|
||||
import chat from './chat';
|
||||
import contacts from './contacts';
|
||||
|
||||
const CancelToken = axios.CancelToken;
|
||||
const headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36',
|
||||
'client-version': '2.0.0',
|
||||
extspam: 'Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA==',
|
||||
referer: 'https://wx.qq.com/?&lang=zh_CN&target=t',
|
||||
};
|
||||
|
||||
class Session {
|
||||
@observable loading = true;
|
||||
@observable auth;
|
||||
@observable code;
|
||||
@observable avatar;
|
||||
@observable user;
|
||||
|
||||
syncKey;
|
||||
|
||||
genSyncKey(list) {
|
||||
return (self.syncKey = list.map(e => `${e.Key}_${e.Val}`).join('|'));
|
||||
}
|
||||
|
||||
@action async getCode() {
|
||||
var response = await axios.get('https://login.wx.qq.com/jslogin', {
|
||||
params: {
|
||||
appid: 'wx782c26e4c19acffb',
|
||||
fun: 'new',
|
||||
redirect_uri: 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop',
|
||||
lang: 'zh_CN'
|
||||
},
|
||||
headers: headers,
|
||||
});
|
||||
// var response = await axios.get('https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=en_US&_=' + +new Date());
|
||||
var code = response.data.match(/[A-Za-z_\-\d]{10}==/)[0];
|
||||
|
||||
self.code = code;
|
||||
self.check();
|
||||
return code;
|
||||
}
|
||||
@action async check() {
|
||||
// Already logined
|
||||
if (self.auth) return;
|
||||
|
||||
var response = await axios.get('https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login', {
|
||||
params: {
|
||||
loginicon: true,
|
||||
uuid: self.code,
|
||||
tip: 1,
|
||||
r: ~new Date(),
|
||||
_: +new Date(),
|
||||
},
|
||||
headers: headers
|
||||
});
|
||||
eval(response.data);
|
||||
|
||||
switch (window.code) {
|
||||
case 200:
|
||||
let authAddress = window.redirect_uri;
|
||||
|
||||
// Set your weChat network route, otherwise you will got a code '1102'
|
||||
axios.defaults.baseURL = authAddress.match(/^https:\/\/(.*?)\//)[0];
|
||||
|
||||
delete window.redirect_uri;
|
||||
delete window.code;
|
||||
delete window.userAvatar;
|
||||
|
||||
// Login success, create session
|
||||
let response = await axios.get(authAddress, {
|
||||
params: {
|
||||
fun: 'new',
|
||||
version: 'v2',
|
||||
},
|
||||
headers: headers
|
||||
});
|
||||
let auth = {};
|
||||
|
||||
try {
|
||||
auth = {
|
||||
baseURL: axios.defaults.baseURL,
|
||||
skey: response.data.match(/<skey>(.*?)<\/skey>/)[1],
|
||||
passTicket: response.data.match(/<pass_ticket>(.*?)<\/pass_ticket>/)[1],
|
||||
wxsid: response.data.match(/<wxsid>(.*?)<\/wxsid>/)[1],
|
||||
wxuin: response.data.match(/<wxuin>(.*?)<\/wxuin>/)[1],
|
||||
};
|
||||
} catch (ex) {
|
||||
window.alert('Your login may be compromised. For account security, you cannot log in to Web WeChat. You can try mobile WeChat or Windows WeChat.');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
self.auth = auth;
|
||||
await storage.set('auth', auth);
|
||||
await self.initUser();
|
||||
self.keepalive().catch(ex => self.logout());
|
||||
break;
|
||||
|
||||
case 201:
|
||||
// Confirm to login
|
||||
self.avatar = window.userAvatar;
|
||||
self.check();
|
||||
break;
|
||||
|
||||
case 400:
|
||||
// QR Code has expired
|
||||
window.location.reload();
|
||||
return;
|
||||
|
||||
default:
|
||||
// Continue call server and waite
|
||||
self.check();
|
||||
}
|
||||
}
|
||||
|
||||
@action async initUser() {
|
||||
var response = await axios.post(`/cgi-bin/mmwebwx-bin/webwxinit?r=${-new Date()}&pass_ticket=${self.auth.passTicket}`, {
|
||||
BaseRequest: {
|
||||
Sid: self.auth.wxsid,
|
||||
Uin: self.auth.wxuin,
|
||||
Skey: self.auth.skey,
|
||||
}
|
||||
});
|
||||
|
||||
await axios.post(`/cgi-bin/mmwebwx-bin/webwxstatusnotify?lang=en_US&pass_ticket=${self.auth.passTicket}`, {
|
||||
BaseRequest: {
|
||||
Sid: self.auth.wxsid,
|
||||
Uin: self.auth.wxuin,
|
||||
Skey: self.auth.skey,
|
||||
},
|
||||
ClientMsgId: +new Date(),
|
||||
Code: 3,
|
||||
FromUserName: response.data.User.UserName,
|
||||
ToUserName: response.data.User.UserName,
|
||||
});
|
||||
|
||||
self.user = response.data;
|
||||
self.user.ContactList.map(e => {
|
||||
e.HeadImgUrl = `${axios.defaults.baseURL}${e.HeadImgUrl.substr(1)}`;
|
||||
});
|
||||
await contacts.getContats();
|
||||
await chat.loadChats(self.user.ChatSet);
|
||||
|
||||
return self.user;
|
||||
}
|
||||
|
||||
async getNewMessage() {
|
||||
var auth = self.auth;
|
||||
var response = await axios.post(`/cgi-bin/mmwebwx-bin/webwxsync?sid=${auth.wxsid}&skey=${auth.skey}&lang=en_US&pass_ticket=${auth.passTicket}`, {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
SyncKey: self.user.SyncKey,
|
||||
rr: ~new Date(),
|
||||
});
|
||||
var mods = [];
|
||||
|
||||
// Refresh the sync keys
|
||||
self.user.SyncKey = response.data.SyncCheckKey;
|
||||
self.genSyncKey(response.data.SyncCheckKey.List);
|
||||
|
||||
// Get the new friend, or chat room has change
|
||||
response.data.ModContactList.map(e => {
|
||||
var hasUser = contacts.memberList.find(user => user.UserName === e.UserName);
|
||||
|
||||
if (hasUser) {
|
||||
// Just update the user
|
||||
contacts.updateUser(e);
|
||||
} else {
|
||||
// If user not exists put it in batch list
|
||||
mods.push(e.UserName);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete user
|
||||
response.data.DelContactList.map((e) => {
|
||||
contacts.deleteUser(e.UserName);
|
||||
chat.removeChat(e);
|
||||
});
|
||||
|
||||
if (mods.length) {
|
||||
await contacts.batch(mods, true);
|
||||
}
|
||||
|
||||
response.data.AddMsgList.map(e => {
|
||||
var from = e.FromUserName;
|
||||
var to = e.ToUserName;
|
||||
var fromYourPhone = from === self.user.User.UserName && from !== to;
|
||||
|
||||
// When message has been readed on your phone, will receive this message
|
||||
if (e.MsgType === 51) {
|
||||
return chat.markedRead(fromYourPhone ? from : to);
|
||||
}
|
||||
|
||||
e.Content = normalize(e.Content);
|
||||
|
||||
// Sync message from your phone
|
||||
if (fromYourPhone) {
|
||||
// Message is sync from your phone
|
||||
chat.addMessage(e, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (from.startsWith('@')) {
|
||||
chat.addMessage(e);
|
||||
}
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// A callback for cancel the sync request
|
||||
cancelCheck = window.Function;
|
||||
|
||||
checkTimeout(weakup) {
|
||||
// Kill the zombie request or duplicate request
|
||||
self.cancelCheck();
|
||||
clearTimeout(self.checkTimeout.timer);
|
||||
|
||||
if (helper.isSuspend() || weakup) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.checkTimeout.timer = setTimeout(() => {
|
||||
self.cancelCheck();
|
||||
}, 30 * 1000);
|
||||
}
|
||||
|
||||
async keepalive() {
|
||||
var auth = self.auth;
|
||||
var response = await axios.post(`/cgi-bin/mmwebwx-bin/webwxsync?sid=${auth.wxsid}&skey=${auth.skey}&lang=en_US&pass_ticket=${auth.passTicket}`, {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
SyncKey: self.user.SyncKey,
|
||||
rr: ~new Date(),
|
||||
});
|
||||
var host = axios.defaults.baseURL.replace('//', '//webpush.');
|
||||
var loop = async() => {
|
||||
// Start detect timeout
|
||||
self.checkTimeout();
|
||||
|
||||
var response = await axios.get(`${host}cgi-bin/mmwebwx-bin/synccheck`, {
|
||||
cancelToken: new CancelToken(exe => {
|
||||
// An executor function receives a cancel function as a parameter
|
||||
this.cancelCheck = exe;
|
||||
}),
|
||||
params: {
|
||||
r: +new Date(),
|
||||
sid: auth.wxsid,
|
||||
uin: auth.wxuin,
|
||||
skey: auth.skey,
|
||||
synckey: self.syncKey,
|
||||
}
|
||||
}).catch(ex => {
|
||||
if (axios.isCancel(ex)) {
|
||||
loop();
|
||||
} else {
|
||||
self.logout();
|
||||
}
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
// Request has been canceled
|
||||
return;
|
||||
}
|
||||
|
||||
eval(response.data);
|
||||
|
||||
if (+window.synccheck.retcode === 0) {
|
||||
// 2, Has new message
|
||||
// 6, New friend
|
||||
// 4, Conversation refresh ?
|
||||
// 7, Exit or enter
|
||||
let selector = +window.synccheck.selector;
|
||||
|
||||
if (selector !== 0) {
|
||||
await self.getNewMessage();
|
||||
}
|
||||
|
||||
// Do next sync keep your wechat alive
|
||||
return loop();
|
||||
} else {
|
||||
self.logout();
|
||||
}
|
||||
};
|
||||
|
||||
// Load the rencets chats
|
||||
response.data.AddMsgList.map(
|
||||
async e => {
|
||||
await chat.loadChats(e.StatusNotifyUserName);
|
||||
}
|
||||
);
|
||||
|
||||
self.loading = false;
|
||||
self.genSyncKey(response.data.SyncCheckKey.List);
|
||||
|
||||
return loop();
|
||||
}
|
||||
|
||||
@action async hasLogin() {
|
||||
var auth = await storage.get('auth');
|
||||
|
||||
axios.defaults.baseURL = auth.baseURL;
|
||||
|
||||
self.auth = auth && Object.keys(auth).length ? auth : void 0;
|
||||
|
||||
if (self.auth) {
|
||||
await self.initUser().catch(ex => self.logout());
|
||||
self.keepalive().catch(ex => self.logout());
|
||||
}
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
@action async logout() {
|
||||
var auth = self.auth;
|
||||
|
||||
try {
|
||||
await axios.post(`/cgi-bin/mmwebwx-bin/webwxlogout?skey=${auth.skey}&redirect=0&type=1`, {
|
||||
sid: auth.sid,
|
||||
uin: auth.uid,
|
||||
});
|
||||
} finally {
|
||||
self.exit();
|
||||
}
|
||||
}
|
||||
|
||||
async exit() {
|
||||
await storage.remove('auth');
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Session();
|
||||
export default self;
|
||||
138
src/js/stores/settings.js
Normal file
138
src/js/stores/settings.js
Normal file
@@ -0,0 +1,138 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import { remote, ipcRenderer } from 'electron';
|
||||
|
||||
import storage from 'utils/storage';
|
||||
import helper from 'utils/helper';
|
||||
|
||||
class Settings {
|
||||
@observable alwaysOnTop = false;
|
||||
@observable showOnTray = false;
|
||||
@observable showNotification = true;
|
||||
@observable confirmImagePaste = true;
|
||||
@observable startup = false;
|
||||
@observable blockRecall = false;
|
||||
@observable rememberConversation = false;
|
||||
@observable showRedIcon = true;
|
||||
@observable downloads = '';
|
||||
|
||||
@action setAlwaysOnTop(alwaysOnTop) {
|
||||
self.alwaysOnTop = alwaysOnTop;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setShowRedIcon(showRedIcon) {
|
||||
self.showRedIcon = showRedIcon;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setRememberConversation(rememberConversation) {
|
||||
self.rememberConversation = rememberConversation;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setBlockRecall(blockRecall) {
|
||||
self.blockRecall = blockRecall;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setShowOnTray(showOnTray) {
|
||||
self.showOnTray = showOnTray;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setConfirmImagePaste(confirmImagePaste) {
|
||||
self.confirmImagePaste = confirmImagePaste;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setShowNotification(showNotification) {
|
||||
self.showNotification = showNotification;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setStartup(startup) {
|
||||
self.startup = startup;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action setDownloads(downloads) {
|
||||
self.downloads = downloads.path;
|
||||
self.save();
|
||||
}
|
||||
|
||||
@action async init() {
|
||||
var settings = await storage.get('settings');
|
||||
var { alwaysOnTop, showOnTray, showNotification, blockRecall, rememberConversation, showRedIcon, startup, downloads } = self;
|
||||
|
||||
if (settings && Object.keys(settings).length) {
|
||||
// Use !! force convert to a bool value
|
||||
self.alwaysOnTop = !!settings.alwaysOnTop;
|
||||
self.showOnTray = !!settings.showOnTray;
|
||||
self.showNotification = !!settings.showNotification;
|
||||
self.confirmImagePaste = !!settings.confirmImagePaste;
|
||||
self.startup = !!settings.startup;
|
||||
self.blockRecall = !!settings.blockRecall;
|
||||
self.rememberConversation = !!settings.rememberConversation;
|
||||
self.showRedIcon = !!settings.showRedIcon;
|
||||
self.downloads = settings.downloads;
|
||||
} else {
|
||||
await storage.set('settings', {
|
||||
alwaysOnTop,
|
||||
showOnTray,
|
||||
showNotification,
|
||||
startup,
|
||||
downloads,
|
||||
blockRecall,
|
||||
rememberConversation,
|
||||
showRedIcon,
|
||||
});
|
||||
}
|
||||
|
||||
// Alway show the tray icon on windows
|
||||
if (!helper.isOsx) {
|
||||
self.showOnTray = true;
|
||||
}
|
||||
|
||||
if (!self.downloads
|
||||
|| typeof self.downloads !== 'string') {
|
||||
self.downloads = remote.app.getPath('downloads');
|
||||
}
|
||||
|
||||
self.save();
|
||||
return settings;
|
||||
}
|
||||
|
||||
save() {
|
||||
var { alwaysOnTop, showOnTray, showNotification, confirmImagePaste, blockRecall, rememberConversation, showRedIcon, startup, downloads } = self;
|
||||
|
||||
storage.set('settings', {
|
||||
alwaysOnTop,
|
||||
showOnTray,
|
||||
showNotification,
|
||||
confirmImagePaste,
|
||||
startup,
|
||||
downloads,
|
||||
blockRecall,
|
||||
rememberConversation,
|
||||
showRedIcon,
|
||||
});
|
||||
|
||||
ipcRenderer.send('settings-apply', {
|
||||
settings: {
|
||||
alwaysOnTop,
|
||||
showOnTray,
|
||||
showNotification,
|
||||
confirmImagePaste,
|
||||
startup,
|
||||
downloads,
|
||||
blockRecall,
|
||||
rememberConversation,
|
||||
showRedIcon,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Settings();
|
||||
export default self;
|
||||
30
src/js/stores/snackbar.js
Normal file
30
src/js/stores/snackbar.js
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
|
||||
class Snackbar {
|
||||
@observable show = false;
|
||||
@observable text = '';
|
||||
|
||||
timer;
|
||||
|
||||
@action toggle(show = self.show, text = self.text) {
|
||||
self.show = show;
|
||||
self.text = text;
|
||||
|
||||
if (show) {
|
||||
clearTimeout(self.timer);
|
||||
self.timer = setTimeout(() => {
|
||||
self.toggle(false);
|
||||
}, 3000);
|
||||
} else {
|
||||
clearTimeout(self.timer);
|
||||
}
|
||||
}
|
||||
|
||||
@action showMessage(text = '') {
|
||||
self.toggle(true, text);
|
||||
}
|
||||
}
|
||||
|
||||
const self = new Snackbar();
|
||||
export default self;
|
||||
77
src/js/stores/userinfo.js
Normal file
77
src/js/stores/userinfo.js
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
import { observable, action } from 'mobx';
|
||||
import axios from 'axios';
|
||||
|
||||
import session from './session';
|
||||
import helper from 'utils/helper';
|
||||
import storage from 'utils/storage';
|
||||
|
||||
class UserInfo {
|
||||
@observable show = false;
|
||||
@observable remove = false;
|
||||
@observable user = {};
|
||||
@observable pallet = [];
|
||||
|
||||
@action async toggle(show = self.show, user = self.user, remove = false) {
|
||||
if (user.UserName === session.user.User.UserName) {
|
||||
remove = false;
|
||||
}
|
||||
|
||||
self.remove = remove;
|
||||
self.show = show;
|
||||
self.user = user;
|
||||
|
||||
// Try to get from cache
|
||||
var pallet = user.pallet;
|
||||
|
||||
if (show) {
|
||||
if (pallet) {
|
||||
self.pallet = user.pallet;
|
||||
} else {
|
||||
pallet = await helper.getPallet(user.HeadImgUrl);
|
||||
|
||||
// Cache the pallet
|
||||
self.user.pallet = pallet;
|
||||
self.pallet = pallet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action updateUser(user) {
|
||||
self.user = user;
|
||||
}
|
||||
|
||||
@action async setRemarkName(name, id) {
|
||||
var auth = await storage.get('auth');
|
||||
var response = await axios.post('/cgi-bin/mmwebwx-bin/webwxoplog', {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
CmdId: 2,
|
||||
RemarkName: name.trim(),
|
||||
UserName: id,
|
||||
});
|
||||
|
||||
return +response.data.BaseResponse.Ret === 0;
|
||||
}
|
||||
|
||||
@action async removeMember(roomId, userid) {
|
||||
var auth = await storage.get('auth');
|
||||
var response = await axios.post('/cgi-bin/mmwebwx-bin/webwxupdatechatroom?fun=delmember', {
|
||||
BaseRequest: {
|
||||
Sid: auth.wxsid,
|
||||
Uin: auth.wxuin,
|
||||
Skey: auth.skey,
|
||||
},
|
||||
ChatRoomName: roomId,
|
||||
DelMemberList: userid,
|
||||
});
|
||||
|
||||
return +response.data.BaseResponse.Ret === 0;
|
||||
}
|
||||
}
|
||||
|
||||
const self = new UserInfo();
|
||||
export default self;
|
||||
259
src/js/utils/albumcolors.js
Normal file
259
src/js/utils/albumcolors.js
Normal file
@@ -0,0 +1,259 @@
|
||||
|
||||
/* eslint-disable */
|
||||
// https://github.com/chengyin/albumcolors
|
||||
|
||||
(function() {
|
||||
var colorChooser, AlbumImage, AlbumColors;
|
||||
|
||||
|
||||
/* colorChooser
|
||||
* A series of function that is used to pick up 3 colors among 10 dominating colors
|
||||
*/
|
||||
colorChooser = {
|
||||
colorStringToRGBArray: function(colorString) {
|
||||
var n, arr = colorString.split(',');
|
||||
|
||||
for (n = 0; n < arr.length; n++) {
|
||||
colorString[n] = parseInt(colorString[n], 10);
|
||||
}
|
||||
|
||||
return colorString;
|
||||
},
|
||||
|
||||
colorDistance: function(colorA, colorB) {
|
||||
var colorARGB = colorA,
|
||||
colorBRGB = colorB,
|
||||
distance = 0,
|
||||
n;
|
||||
|
||||
for (n = 0; n < colorARGB.length; n++) {
|
||||
distance += (colorARGB[n] - colorBRGB[n]) * (colorARGB[n] - colorBRGB[n]);
|
||||
}
|
||||
|
||||
return Math.sqrt(distance);
|
||||
},
|
||||
|
||||
getDistances: function(colors) {
|
||||
var distances = [],
|
||||
c1, c2;
|
||||
|
||||
for (c1 = 0; c1 < colors.length; c1++) {
|
||||
distances[c1] = [];
|
||||
for (c2 = 0; c2 < colors.length; c2++) {
|
||||
distances[c1][c2] = colorChooser.colorDistance(colors[c1], colors[c2]);
|
||||
}
|
||||
}
|
||||
|
||||
return distances;
|
||||
},
|
||||
|
||||
chooseThreeColors: function(colors) {
|
||||
var color1 = colors[0],
|
||||
colorDistances = colorChooser.getDistances(colors),
|
||||
color2Index = 0,
|
||||
color3Index = 1, c, c2, color2, color3;
|
||||
|
||||
for (c = 0; c < colors.length; c++) {
|
||||
if (colorDistances[0][c] > colorDistances[0][color2Index]) {
|
||||
color2Index = c;
|
||||
}
|
||||
}
|
||||
|
||||
color2 = colors[color2Index];
|
||||
color3Index = color2Index + 1;
|
||||
if (color3Index >= colors.length) {
|
||||
color3Index = 1;
|
||||
}
|
||||
|
||||
for (c = 1; c < colors.length; c++) {
|
||||
if (c !== color2Index && colorDistances[0][c] > colorDistances[0][color3Index]) {
|
||||
color3Index = c;
|
||||
}
|
||||
}
|
||||
|
||||
color3 = colors[color3Index];
|
||||
|
||||
return [color1, color2, color3];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* A Class for the to wrap image,
|
||||
* used for counting raw color pixels
|
||||
*/
|
||||
AlbumImage = function(url) {
|
||||
this.url = url;
|
||||
};
|
||||
|
||||
AlbumImage.prototype.fetch = function(callback) {
|
||||
var that = this;
|
||||
|
||||
this.image = new Image();
|
||||
|
||||
this.image.onload = function() {
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
};
|
||||
|
||||
this.image.src = this.url;
|
||||
};
|
||||
|
||||
AlbumImage.prototype.getCanvas = function() {
|
||||
if (this.canvas) {
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
var canvas, context;
|
||||
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.width = this.image.width;
|
||||
canvas.height = this.image.height;
|
||||
context = canvas.getContext('2d');
|
||||
|
||||
context.drawImage(this.image, 0, 0);
|
||||
|
||||
return (this.canvas = canvas);
|
||||
};
|
||||
|
||||
AlbumImage.prototype.getPixelArray = function() {
|
||||
return this.getCanvas().getContext('2d').getImageData(0, 0, this.image.width, this.image.height).data;
|
||||
};
|
||||
|
||||
AlbumImage.prototype.getColors = function() {
|
||||
if (this.colors) {
|
||||
return this.colors;
|
||||
}
|
||||
|
||||
var p, colors = [],
|
||||
pixelArray = this.getPixelArray();
|
||||
|
||||
for (p = 0; p < pixelArray.length; p += 4) {
|
||||
colors.push([pixelArray[p], pixelArray[p + 1], pixelArray[p + 2]]);
|
||||
}
|
||||
|
||||
return (this.colors = colors);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* AlbumColors
|
||||
* Generate pallete among dominating colors
|
||||
*/
|
||||
AlbumColors = function(imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
this.image = new AlbumImage(imageUrl);
|
||||
};
|
||||
|
||||
AlbumColors.prototype.getColors = function(callback) {
|
||||
var that = this;
|
||||
this.image.fetch(function() {
|
||||
that.colors = that.extractMainColors(10);
|
||||
if (callback) {
|
||||
callback(colorChooser.chooseThreeColors(that.colors));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AlbumColors.prototype.generateRGBString = function(color) {
|
||||
return color.join(',');
|
||||
};
|
||||
|
||||
AlbumColors.prototype.getBucket = function(color) {
|
||||
// Throw a color into one color bucket
|
||||
var bucket = [],
|
||||
c;
|
||||
|
||||
for (c = 0; c < color.length; c++) {
|
||||
// Naive
|
||||
bucket[c] = Math.round(color[c] / 64) * 64;
|
||||
}
|
||||
|
||||
return bucket;
|
||||
};
|
||||
|
||||
AlbumColors.prototype.getColorsByBucket = function() {
|
||||
if (this.colorsByBucket) {
|
||||
return this.colorsByBucket;
|
||||
}
|
||||
|
||||
var colors, c, color, bucket, colorsByBucket, rgbString;
|
||||
|
||||
colors = this.image.getColors();
|
||||
colorsByBucket = {};
|
||||
|
||||
for (c = 0; c < colors.length; c++) {
|
||||
color = colors[c];
|
||||
bucket = this.getBucket(color);
|
||||
rgbString = this.generateRGBString(bucket);
|
||||
|
||||
colorsByBucket[rgbString] = colorsByBucket[rgbString] || [];
|
||||
if (colorsByBucket[rgbString]) {
|
||||
colorsByBucket[rgbString].push(color);
|
||||
}
|
||||
}
|
||||
|
||||
return (this.colorsByBucket = colorsByBucket);
|
||||
};
|
||||
|
||||
AlbumColors.prototype.getAverageColor = function(colors) {
|
||||
var c, r = 0,
|
||||
g = 0,
|
||||
b = 0;
|
||||
|
||||
for (c = 0; c < colors.length; c++) {
|
||||
r += colors[c][0];
|
||||
g += colors[c][1];
|
||||
b += colors[c][2];
|
||||
}
|
||||
|
||||
r = parseInt(r / colors.length, 10);
|
||||
g = parseInt(g / colors.length, 10);
|
||||
b = parseInt(b / colors.length, 10);
|
||||
|
||||
return [r, g, b];
|
||||
};
|
||||
|
||||
AlbumColors.prototype.getColorBuckets = function() {
|
||||
if (this.colorBuckets) {
|
||||
return this.colorBuckets;
|
||||
}
|
||||
|
||||
var colorsByBucket = this.getColorsByBucket(),
|
||||
bucket, buckets = [];
|
||||
|
||||
for (bucket in colorsByBucket) {
|
||||
if (colorsByBucket.hasOwnProperty(bucket)) {
|
||||
buckets.push(bucket);
|
||||
}
|
||||
}
|
||||
|
||||
return (this.colorBuckets = buckets);
|
||||
};
|
||||
|
||||
AlbumColors.prototype.extractMainColors = function(count) {
|
||||
if (!this.mainColors) {
|
||||
var colorsByBucket = this.getColorsByBucket(),
|
||||
colorBuckets = this.getColorBuckets().slice(0),
|
||||
b, mainColors = [];
|
||||
|
||||
colorBuckets.sort(function(colorBucketA, colorBucketB) {
|
||||
return colorsByBucket[colorBucketB].length - colorsByBucket[colorBucketA].length;
|
||||
});
|
||||
|
||||
for (b = 0; b < colorBuckets.length; b++) {
|
||||
mainColors.push(this.getAverageColor(colorsByBucket[colorBuckets[b]]));
|
||||
}
|
||||
|
||||
this.mainColors = mainColors;
|
||||
}
|
||||
|
||||
return this.mainColors.slice(0, count);
|
||||
};
|
||||
|
||||
AlbumColors.AlbumImage = AlbumImage;
|
||||
|
||||
window.AlbumColors = AlbumColors;
|
||||
}());
|
||||
13
src/js/utils/blacklist.js
Normal file
13
src/js/utils/blacklist.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
export default function blacklist(src, ...args) {
|
||||
var copy = {};
|
||||
var ignore = Array.from(args);
|
||||
|
||||
for (var key in src) {
|
||||
if (ignore.indexOf(key) === -1) {
|
||||
copy[key] = src[key];
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
400
src/js/utils/emoji.js
Normal file
400
src/js/utils/emoji.js
Normal file
@@ -0,0 +1,400 @@
|
||||
|
||||
const EmojiList = ['笑脸', '生病', '破涕为笑', '吐舌', '脸红', '恐惧', '失望', '无语', '嘿哈', '捂脸', '奸笑', '机智', '皱眉', '耶', '鬼魂', '合十', '强壮', '庆祝', '礼物', '红包', '鸡', '开心', '大笑', '热情', '眨眼', '色', '接吻', '亲吻', '露齿笑', '满意', '戏弄', '得意', '汗', '低落', '呸', '焦虑', '担心', '震惊', '悔恨', '眼泪', '哭', '晕', '心烦', '生气', '睡觉', '恶魔', '外星人', '心', '心碎', '丘比特', '闪烁', '星星', '叹号', '问号', '睡着', '水滴', '音乐', '火', '便便', '弱', '拳头', '胜利', '上', '下', '右', '左', '第一', '吻', '热恋', '男孩', '女孩', '女士', '男士', '天使', '骷髅', '红唇', '太阳', '下雨', '多云', '雪人', '月亮', '闪电', '海浪', '猫', '小狗', '老鼠', '仓鼠', '兔子', '狗', '青蛙', '老虎', '考拉', '熊', '猪', '牛', '野猪', '猴子', '马', '蛇', '鸽子', '鸡', '企鹅', '毛虫', '章鱼', '鱼', '鲸鱼', '海豚', '玫瑰', '花', '棕榈树', '仙人掌', '礼盒', '南瓜灯', '圣诞老人', '圣诞树', '礼物', '铃', '气球', 'CD', '相机', '录像机', '电脑', '电视', '电话', '解锁', '锁', '钥匙', '成交', '灯泡', '邮箱', '浴缸', '钱', '炸弹', '手枪', '药丸', '橄榄球', '篮球', '足球', '棒球', '高尔夫', '奖杯', '入侵者', '唱歌', '吉他', '比基尼', '皇冠', '雨伞', '手提包', '口红', '戒指', '钻石', '咖啡', '啤酒', '干杯', '鸡尾酒', '汉堡', '薯条', '意面', '寿司', '面条', '煎蛋', '冰激凌', '蛋糕', '苹果', '飞机', '火箭', '自行车', '高铁', '警告', '旗', '男人', '女人', 'O', 'X', '版权', '注册商标', '商标'];
|
||||
const QQFaceMap = {
|
||||
'微笑': '0',
|
||||
'撇嘴': '1',
|
||||
'色': '2',
|
||||
'发呆': '3',
|
||||
'得意': '4',
|
||||
'流泪': '5',
|
||||
'害羞': '6',
|
||||
'闭嘴': '7',
|
||||
'睡': '8',
|
||||
'大哭': '9',
|
||||
'尴尬': '10',
|
||||
'发怒': '11',
|
||||
'调皮': '12',
|
||||
'呲牙': '13',
|
||||
'惊讶': '14',
|
||||
'难过': '15',
|
||||
'酷': '16',
|
||||
'冷汗': '17',
|
||||
'抓狂': '18',
|
||||
'吐': '19',
|
||||
'偷笑': '20',
|
||||
'可爱': '21',
|
||||
'愉快': '21',
|
||||
'白眼': '22',
|
||||
'傲慢': '23',
|
||||
'饥饿': '24',
|
||||
'困': '25',
|
||||
'惊恐': '26',
|
||||
'流汗': '27',
|
||||
'憨笑': '28',
|
||||
'悠闲': '29',
|
||||
'大兵': '29',
|
||||
'奋斗': '30',
|
||||
'咒骂': '31',
|
||||
'疑问': '32',
|
||||
'嘘': '33',
|
||||
'晕': '34',
|
||||
'疯了': '35',
|
||||
'折磨': '35',
|
||||
'衰': '36',
|
||||
'骷髅': '37',
|
||||
'敲打': '38',
|
||||
'再见': '39',
|
||||
'擦汗': '40',
|
||||
'抠鼻': '41',
|
||||
'鼓掌': '42',
|
||||
'糗大了': '43',
|
||||
'坏笑': '44',
|
||||
'左哼哼': '45',
|
||||
'右哼哼': '46',
|
||||
'哈欠': '47',
|
||||
'鄙视': '48',
|
||||
'委屈': '49',
|
||||
'快哭了': '50',
|
||||
'阴险': '51',
|
||||
'亲亲': '52',
|
||||
'吓': '53',
|
||||
'可怜': '54',
|
||||
'菜刀': '55',
|
||||
'西瓜': '56',
|
||||
'啤酒': '57',
|
||||
'篮球': '58',
|
||||
'乒乓': '59',
|
||||
'咖啡': '60',
|
||||
'饭': '61',
|
||||
'猪头': '62',
|
||||
'玫瑰': '63',
|
||||
'凋谢': '64',
|
||||
'嘴唇': '65',
|
||||
'示爱': '65',
|
||||
'爱心': '66',
|
||||
'心碎': '67',
|
||||
'蛋糕': '68',
|
||||
'闪电': '69',
|
||||
'炸弹': '70',
|
||||
'刀': '71',
|
||||
'足球': '72',
|
||||
'瓢虫': '73',
|
||||
'便便': '74',
|
||||
'月亮': '75',
|
||||
'太阳': '76',
|
||||
'礼物': '77',
|
||||
'拥抱': '78',
|
||||
'强': '79',
|
||||
'弱': '80',
|
||||
'握手': '81',
|
||||
'胜利': '82',
|
||||
'抱拳': '83',
|
||||
'勾引': '84',
|
||||
'拳头': '85',
|
||||
'差劲': '86',
|
||||
'爱你': '87',
|
||||
NO: '88',
|
||||
OK: '89',
|
||||
'爱情': '90',
|
||||
'飞吻': '91',
|
||||
'跳跳': '92',
|
||||
'发抖': '93',
|
||||
'怄火': '94',
|
||||
'转圈': '95',
|
||||
'磕头': '96',
|
||||
'回头': '97',
|
||||
'跳绳': '98',
|
||||
'投降': '99',
|
||||
'激动': '100',
|
||||
'乱舞': '101',
|
||||
'献吻': '102',
|
||||
'左太极': '103',
|
||||
'右太极': '104',
|
||||
'嘿哈': '105',
|
||||
'捂脸': '106',
|
||||
'奸笑': '107',
|
||||
'机智': '108',
|
||||
'皱眉': '109',
|
||||
'耶': '110',
|
||||
'鸡': '111',
|
||||
'红包': '112',
|
||||
Smile: '0',
|
||||
Grimace: '1',
|
||||
Drool: '2',
|
||||
Scowl: '3',
|
||||
Chill: '4',
|
||||
CoolGuy: '4',
|
||||
Sob: '5',
|
||||
Shy: '6',
|
||||
Shutup: '7',
|
||||
Silent: '7',
|
||||
Sleep: '8',
|
||||
Cry: '9',
|
||||
Awkward: '10',
|
||||
Pout: '11',
|
||||
Angry: '11',
|
||||
Wink: '12',
|
||||
Tongue: '12',
|
||||
Grin: '13',
|
||||
Surprised: '14',
|
||||
Surprise: '14',
|
||||
Frown: '15',
|
||||
Cool: '16',
|
||||
Ruthless: '16',
|
||||
Tension: '17',
|
||||
Blush: '17',
|
||||
Scream: '18',
|
||||
Crazy: '18',
|
||||
Puke: '19',
|
||||
Chuckle: '20',
|
||||
Joyful: '21',
|
||||
Slight: '22',
|
||||
Smug: '23',
|
||||
Hungry: '24',
|
||||
Drowsy: '25',
|
||||
Panic: '26',
|
||||
Sweat: '27',
|
||||
Laugh: '28',
|
||||
Loafer: '29',
|
||||
Commando: '29',
|
||||
Strive: '30',
|
||||
Determined: '30',
|
||||
Scold: '31',
|
||||
Doubt: '32',
|
||||
Shocked: '32',
|
||||
Shhh: '33',
|
||||
Dizzy: '34',
|
||||
Tormented: '35',
|
||||
BadLuck: '36',
|
||||
Toasted: '36',
|
||||
Skull: '37',
|
||||
Hammer: '38',
|
||||
Wave: '39',
|
||||
Relief: '40',
|
||||
Speechless: '40',
|
||||
DigNose: '41',
|
||||
NosePick: '41',
|
||||
Clap: '42',
|
||||
Shame: '43',
|
||||
Trick: '44',
|
||||
'Bah!L': '45',
|
||||
'Bah!R': '46',
|
||||
Yawn: '47',
|
||||
Lookdown: '48',
|
||||
'Pooh-pooh': '48',
|
||||
Wronged: '49',
|
||||
Shrunken: '49',
|
||||
Puling: '50',
|
||||
TearingUp: '50',
|
||||
Sly: '51',
|
||||
Kiss: '52',
|
||||
'Uh-oh': '53',
|
||||
Wrath: '53',
|
||||
Whimper: '54',
|
||||
Cleaver: '55',
|
||||
Melon: '56',
|
||||
Watermelon: '56',
|
||||
Beer: '57',
|
||||
Basketball: '58',
|
||||
PingPong: '59',
|
||||
Coffee: '60',
|
||||
Rice: '61',
|
||||
Pig: '62',
|
||||
Rose: '63',
|
||||
Wilt: '64',
|
||||
Lip: '65',
|
||||
Lips: '65',
|
||||
Heart: '66',
|
||||
BrokenHeart: '67',
|
||||
Cake: '68',
|
||||
Lightning: '69',
|
||||
Bomb: '70',
|
||||
Dagger: '71',
|
||||
Soccer: '72',
|
||||
Ladybug: '73',
|
||||
Poop: '74',
|
||||
Moon: '75',
|
||||
Sun: '76',
|
||||
Gift: '77',
|
||||
Hug: '78',
|
||||
Strong: '79',
|
||||
ThumbsUp: '79',
|
||||
Weak: '80',
|
||||
ThumbsDown: '80',
|
||||
Shake: '81',
|
||||
Victory: '82',
|
||||
Peace: '82',
|
||||
Admire: '83',
|
||||
Fight: '83',
|
||||
Salute: '83',
|
||||
Beckon: '84',
|
||||
Fist: '85',
|
||||
Pinky: '86',
|
||||
Love: '2',
|
||||
RockOn: '87',
|
||||
No: '88',
|
||||
'Nuh-uh': '88',
|
||||
InLove: '90',
|
||||
Blowkiss: '91',
|
||||
Waddle: '92',
|
||||
Tremble: '93',
|
||||
'Aaagh!': '94',
|
||||
Twirl: '95',
|
||||
Kotow: '96',
|
||||
Lookback: '97',
|
||||
Dramatic: '97',
|
||||
Jump: '98',
|
||||
JumpRope: '98',
|
||||
'Give-in': '99',
|
||||
Surrender: '99',
|
||||
Hooray: '100',
|
||||
HeyHey: '101',
|
||||
Meditate: '101',
|
||||
Smooch: '102',
|
||||
'TaiJi L': '103',
|
||||
'TaiChi L': '103',
|
||||
'TaiJi R': '104',
|
||||
'TaiChi R': '104',
|
||||
Hey: '105',
|
||||
Facepalm: '106',
|
||||
Smirk: '107',
|
||||
Smart: '108',
|
||||
Concerned: '109',
|
||||
'Yeah!': '110',
|
||||
Chicken: '111',
|
||||
Packet: '112',
|
||||
'發呆': '3',
|
||||
'流淚': '5',
|
||||
'閉嘴': '7',
|
||||
'尷尬': '10',
|
||||
'發怒': '11',
|
||||
'調皮': '12',
|
||||
'驚訝': '14',
|
||||
'難過': '15',
|
||||
'饑餓': '24',
|
||||
'累': '25',
|
||||
'驚恐': '26',
|
||||
'悠閑': '29',
|
||||
'奮鬥': '30',
|
||||
'咒罵': '31',
|
||||
'疑問': '32',
|
||||
'噓': '33',
|
||||
'暈': '34',
|
||||
'瘋了': '35',
|
||||
'骷髏頭': '37',
|
||||
'再見': '39',
|
||||
'摳鼻': '41',
|
||||
'羞辱': '43',
|
||||
'壞笑': '44',
|
||||
'鄙視': '48',
|
||||
'陰險': '51',
|
||||
'親親': '52',
|
||||
'嚇': '53',
|
||||
'可憐': '54',
|
||||
'籃球': '58',
|
||||
'飯': '61',
|
||||
'豬頭': '62',
|
||||
'枯萎': '64',
|
||||
'愛心': '66',
|
||||
'閃電': '69',
|
||||
'炸彈': '70',
|
||||
'甲蟲': '73',
|
||||
'太陽': '76',
|
||||
'禮物': '77',
|
||||
'擁抱': '78',
|
||||
'強': '79',
|
||||
'勝利': '82',
|
||||
'拳頭': '85',
|
||||
'差勁': '86',
|
||||
'愛你': '88',
|
||||
'愛情': '90',
|
||||
'飛吻': '91',
|
||||
'發抖': '93',
|
||||
'噴火': '94',
|
||||
'轉圈': '95',
|
||||
'磕頭': '96',
|
||||
'回頭': '97',
|
||||
'跳繩': '98',
|
||||
'激動': '100',
|
||||
'亂舞': '101',
|
||||
'獻吻': '102',
|
||||
'左太極': '103',
|
||||
'右太極': '104',
|
||||
'吼嘿': '105',
|
||||
'掩面': '106',
|
||||
'機智': '108',
|
||||
'皺眉': '109',
|
||||
'歐耶': '110',
|
||||
'雞': '111',
|
||||
'紅包': '112',
|
||||
};
|
||||
|
||||
const emoji = [];
|
||||
|
||||
Object.keys(QQFaceMap).slice(0, 105).map(e => {
|
||||
var id = QQFaceMap[e];
|
||||
|
||||
emoji.push({
|
||||
key: e,
|
||||
value: QQFaceMap[e],
|
||||
className: `qqemoji qqemoji${id}`
|
||||
});
|
||||
});
|
||||
|
||||
function getEmojiClassName(name) {
|
||||
name = name.substring(1, name.length - 1);
|
||||
|
||||
var keys = Object.keys(QQFaceMap);
|
||||
var key = keys.find(e => e === name);
|
||||
|
||||
if (key) {
|
||||
let id = QQFaceMap[key];
|
||||
let emojiCN = keys.find(e => QQFaceMap[e] === id);
|
||||
|
||||
if (+id < 200) {
|
||||
return `qqemoji qqemoji${id}`;
|
||||
}
|
||||
|
||||
if (EmojiList.includes(emojiCN)) {
|
||||
return `emoji emoji${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function parser(text) {
|
||||
var decodeText = text;
|
||||
|
||||
(text.match(/\[[\w\s\u4E00-\u9FCC\u3400-\u4DB5\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29\ud840-\ud868\udc00-\udfff\ud869[\udc00-\uded6\udf00-\udfff\ud86a-\ud86c\udc00-\udfff\ud86d[\udc00-\udf34\udf40-\udfff\ud86e\udc00-\udc1d]+\]/g) || []).map(e => {
|
||||
var className = getEmojiClassName(e);
|
||||
|
||||
if (!className) {
|
||||
// Invalid emoji
|
||||
return;
|
||||
}
|
||||
text = decodeText = text.split(`${e}`).join(`<a class="${className}"></a>`);
|
||||
});
|
||||
|
||||
return normalize(decodeText);
|
||||
}
|
||||
|
||||
function normalize(text = '') {
|
||||
var matchs = text.match(/<span class="emoji emoji[0-9a-fA-F]+"><\/span>/g) || [];
|
||||
var decodeText = text;
|
||||
|
||||
try {
|
||||
matchs.map(e => {
|
||||
// Decode utf16 to emoji
|
||||
var emojiCode = e.match(/emoji([0-9a-fA-F]+)/)[1].substr(0, 5);
|
||||
var emoji = String.fromCodePoint(parseInt(emojiCode, 16));
|
||||
text = decodeText = text.split(e).join(emoji);
|
||||
});
|
||||
} catch (ex) {
|
||||
console.error('Failed decode %s: %o', text, ex);
|
||||
}
|
||||
|
||||
return decodeText;
|
||||
}
|
||||
|
||||
export { emoji, parser, normalize };
|
||||
10
src/js/utils/event.js
Normal file
10
src/js/utils/event.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
export function on(el, events, fn) {
|
||||
(el && events && fn)
|
||||
&& events.split().forEach(e => el.addEventListener(e, fn, false));
|
||||
}
|
||||
|
||||
export function off(el, events, fn) {
|
||||
(el && events && fn)
|
||||
&& events.split().forEach(e => el.removeEventListener(e, fn, false));
|
||||
}
|
||||
311
src/js/utils/helper.js
Normal file
311
src/js/utils/helper.js
Normal file
@@ -0,0 +1,311 @@
|
||||
|
||||
import { remote, ipcRenderer } from 'electron';
|
||||
import axios from 'axios';
|
||||
import MD5 from 'browser-md5-file';
|
||||
|
||||
import session from '../stores/session';
|
||||
|
||||
const CHATROOM_NOTIFY_CLOSE = 0;
|
||||
const CONTACTFLAG_NOTIFYCLOSECONTACT = 512;
|
||||
const MM_USERATTRVERIFYFALG_BIZ_BRAND = 8;
|
||||
const CONTACTFLAG_TOPCONTACT = 2048;
|
||||
const CONTACTFLAG_CONTACT = 1;
|
||||
|
||||
const helper = {
|
||||
isContact: (user) => {
|
||||
if (helper.isFileHelper(user)) return true;
|
||||
|
||||
return user.ContactFlag & CONTACTFLAG_CONTACT
|
||||
|| (session.user && user.UserName === session.user.User.UserName);
|
||||
},
|
||||
|
||||
isChatRoom: (userid) => {
|
||||
return userid && userid.startsWith('@@');
|
||||
},
|
||||
|
||||
isChatRoomOwner: (user) => {
|
||||
return helper.isChatRoom(user.UserName) && user.IsOwner;
|
||||
},
|
||||
|
||||
isChatRoomRemoved: (user) => {
|
||||
return helper.isChatRoom(user.UserName) && user.ContactFlag === 0;
|
||||
},
|
||||
|
||||
isMuted: (user) => {
|
||||
return helper.isChatRoom(user.UserName) ? user.Statues === CHATROOM_NOTIFY_CLOSE : user.ContactFlag & CONTACTFLAG_NOTIFYCLOSECONTACT;
|
||||
},
|
||||
|
||||
isOfficial: (user) => {
|
||||
return !(user.VerifyFlag !== 24 && user.VerifyFlag !== 8 && user.UserName.startsWith('@'));
|
||||
},
|
||||
|
||||
isFileHelper: (user) => user.UserName === 'filehelper',
|
||||
|
||||
isTop: (user) => {
|
||||
if (user.isTop !== void 0) {
|
||||
return user.isTop;
|
||||
}
|
||||
|
||||
return user.ContactFlag & CONTACTFLAG_TOPCONTACT;
|
||||
},
|
||||
|
||||
isBrand: (user) => {
|
||||
return user.VerifyFlag & MM_USERATTRVERIFYFALG_BIZ_BRAND;
|
||||
},
|
||||
|
||||
parseKV: (text) => {
|
||||
var string = text.replace(/</g, '<').replace(/>/g, '>');
|
||||
var matchs = string.match(/(\w+)="([^\s]+)"/g);
|
||||
let res = {};
|
||||
|
||||
matchs.map(e => {
|
||||
var kv = e.replace(/"/g, '').split('=');
|
||||
|
||||
res[kv[0]] = kv[1];
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
parseXml: (text, tagName) => {
|
||||
var parser = new window.DOMParser();
|
||||
var xml = parser.parseFromString(text.replace(/</g, '<').replace(/>/g, '>'), 'text/xml');
|
||||
var value = {};
|
||||
|
||||
tagName = Array.isArray(tagName) ? tagName : [tagName];
|
||||
|
||||
tagName.map(e => {
|
||||
value[e] = xml.getElementsByTagName(e)[0].childNodes[0].nodeValue;
|
||||
});
|
||||
|
||||
return { xml, value };
|
||||
},
|
||||
|
||||
unique: (arr) => {
|
||||
var mappings = {};
|
||||
var res = [];
|
||||
|
||||
arr.map(e => {
|
||||
mappings[e] = true;
|
||||
});
|
||||
|
||||
for (var key in mappings) {
|
||||
if (mappings[key] === true) {
|
||||
res.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
getMessageContent: (message) => {
|
||||
var isChatRoom = helper.isChatRoom(message.FromUserName);
|
||||
var content = message.Content;
|
||||
|
||||
if (isChatRoom && !message.isme) {
|
||||
content = message.Content.split(':<br/>')[1];
|
||||
}
|
||||
|
||||
switch (message.MsgType) {
|
||||
case 1:
|
||||
if (message.location) return '[Location]';
|
||||
// Text message
|
||||
return content.replace(/<br\/>/g, '');
|
||||
|
||||
case 3:
|
||||
// Image
|
||||
return '[Image]';
|
||||
|
||||
case 34:
|
||||
// Image
|
||||
return '[Voice]';
|
||||
|
||||
case 42:
|
||||
// Contact Card
|
||||
return '[Contact Card]';
|
||||
|
||||
case 43:
|
||||
// Video
|
||||
return '[Video]';
|
||||
|
||||
case 47:
|
||||
case 49 + 8:
|
||||
// Emoji
|
||||
return '[Emoji]';
|
||||
|
||||
case 49 + 17:
|
||||
return '🚀 Location sharing, Please check your phone.';
|
||||
|
||||
case 49 + 6:
|
||||
return `🚚 ${message.file.name}`;
|
||||
|
||||
case 49 + 2000:
|
||||
// Transfer
|
||||
return `Money +${message.transfer.money} 💰💰💰`;
|
||||
}
|
||||
},
|
||||
|
||||
getCookie: async(name) => {
|
||||
var value = {
|
||||
name,
|
||||
};
|
||||
var cookies = remote.getCurrentWindow().webContents.session.cookies;
|
||||
|
||||
if (!name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookies.get({ url: axios.defaults.baseURL }, (error, cookies) => {
|
||||
let string = '';
|
||||
|
||||
if (error) {
|
||||
return resolve('');
|
||||
}
|
||||
|
||||
for (var i = cookies.length; --i >= 0;) {
|
||||
let item = cookies[i];
|
||||
string += `${item.name}=${item.value} ;`;
|
||||
}
|
||||
|
||||
resolve(string);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cookies.get(value, (err, cookies) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(cookies[0].value);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
humanSize: (size) => {
|
||||
var value = (size / 1024).toFixed(1);
|
||||
|
||||
if (size > (1024 << 10)) {
|
||||
value = (value / 1024).toFixed(1);
|
||||
return `${value} M`;
|
||||
} else {
|
||||
return `${value} KB`;
|
||||
}
|
||||
},
|
||||
|
||||
getFiletypeIcon: (extension) => {
|
||||
var filename = 'unknow';
|
||||
|
||||
extension = (extension || '').toLowerCase().replace(/^\./, '');
|
||||
|
||||
switch (true) {
|
||||
case ['mp3', 'flac', 'aac', 'm4a', 'wma'].includes(extension):
|
||||
filename = 'audio';
|
||||
break;
|
||||
|
||||
case ['mp4', 'mkv', 'avi', 'flv'].includes(extension):
|
||||
filename = 'audio';
|
||||
break;
|
||||
|
||||
case ['zip', 'rar', 'tar', 'tar.gz'].includes(extension):
|
||||
filename = 'archive';
|
||||
break;
|
||||
|
||||
case ['doc', 'docx'].includes(extension):
|
||||
filename = 'word';
|
||||
break;
|
||||
|
||||
case ['xls', 'xlsx'].includes(extension):
|
||||
filename = 'excel';
|
||||
break;
|
||||
|
||||
case ['ai', 'apk', 'exe', 'ipa', 'pdf', 'ppt', 'psd'].includes(extension):
|
||||
filename = extension;
|
||||
break;
|
||||
}
|
||||
|
||||
return `${filename}.png`;
|
||||
},
|
||||
|
||||
getPallet: (image) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
new window.AlbumColors(image).getColors((colors, err) => {
|
||||
if (err) {
|
||||
resolve([
|
||||
[0, 0, 0],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0],
|
||||
]);
|
||||
} else {
|
||||
resolve(colors);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
decodeHTML: (text = '') => {
|
||||
return text.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
||||
},
|
||||
|
||||
isImage: (ext) => {
|
||||
return ['bmp', 'gif', 'jpeg', 'jpg', 'png'].includes(ext);
|
||||
},
|
||||
|
||||
// 3 types supported: pic, video, doc
|
||||
getMediaType: (ext = '') => {
|
||||
ext = ext.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case helper.isImage(ext):
|
||||
return 'pic';
|
||||
|
||||
case ['mp4'].includes(ext):
|
||||
return 'video';
|
||||
|
||||
default:
|
||||
return 'doc';
|
||||
}
|
||||
},
|
||||
|
||||
getDataURL: (src) => {
|
||||
var image = new window.Image();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.src = src;
|
||||
image.onload = () => {
|
||||
var canvas = document.createElement('canvas');
|
||||
var context = canvas.getContext('2d');
|
||||
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
|
||||
context.drawImage(image, 0, 0, image.width, image.height);
|
||||
resolve(canvas.toDataURL('image/png'));
|
||||
};
|
||||
|
||||
image.onerror = () => {
|
||||
resolve('');
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
isOsx: window.process.platform === 'darwin',
|
||||
|
||||
isSuspend: () => {
|
||||
return ipcRenderer.sendSync('is-suspend');
|
||||
},
|
||||
md5: (file) => {
|
||||
const md = new MD5();
|
||||
return new Promise((resolve, reject) => {
|
||||
md.md5(file, (err, md5) => { resolve(err ? false : md5); }, progress => {});
|
||||
});
|
||||
}
|
||||
// md5: (file) => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// MD5(file, (err, md5) => {
|
||||
// resolve(err ? false : md5);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
export default helper;
|
||||
40
src/js/utils/storage.js
Normal file
40
src/js/utils/storage.js
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
import storage from 'electron-json-storage';
|
||||
|
||||
export default {
|
||||
get: (key) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.get(key, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
set: (key, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.set(key, data, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
remove: (key) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
storage.remove(key, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user