first version
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import dayjs from 'dayjs';
|
||||
import store from '../state/store';
|
||||
import { Contact, Message } from '../state/reducer';
|
||||
|
||||
/** 构建本地消息 */
|
||||
export default function composeMessage({
|
||||
type,
|
||||
target,
|
||||
data,
|
||||
filename,
|
||||
fileSize,
|
||||
localFileUrl,
|
||||
}: {
|
||||
type: 'text' | 'img' | 'emotion' | 'file';
|
||||
target: string;
|
||||
data: any;
|
||||
filename?: string;
|
||||
fileSize?: number;
|
||||
localFileUrl?: string;
|
||||
}): Message {
|
||||
const state = store.getState();
|
||||
const to = state.allContacts.get(target);
|
||||
const isGroupMessage = state.groups.includes(to as Contact);
|
||||
if (!to) {
|
||||
throw Error('目标联系人不存在');
|
||||
}
|
||||
const msg = {
|
||||
type,
|
||||
messageID: Math.random().toString(),
|
||||
fileSize: fileSize,
|
||||
displayFilename: filename,
|
||||
fileUrl: localFileUrl,
|
||||
speakingGroupMemberID: isGroupMessage ? state.user?.id : null,
|
||||
speakingGroupMemberName: isGroupMessage
|
||||
? state.user?.displayName
|
||||
: null,
|
||||
fromID: state.user?.id,
|
||||
fromName: state.user?.displayName,
|
||||
fromNickName: state.user?.nickname,
|
||||
toID: target,
|
||||
toName: to?.displayName,
|
||||
toNickName: to?.nickname,
|
||||
createTimestamp: dayjs().unix(),
|
||||
isGroupMessage: isGroupMessage,
|
||||
isSentBySelf: true,
|
||||
pending: true,
|
||||
percent: 0,
|
||||
} as Message;
|
||||
switch (type) {
|
||||
case 'text':
|
||||
msg.content = data;
|
||||
break;
|
||||
case 'img':
|
||||
msg.content = '[图片]';
|
||||
break;
|
||||
case 'emotion':
|
||||
msg.content = '[表情]';
|
||||
break;
|
||||
case 'file':
|
||||
msg.content = '[文件]' + filename;
|
||||
break;
|
||||
default:
|
||||
msg.content = '不支持的消息类型';
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Message from '../components/Message';
|
||||
import { socket } from '../socket';
|
||||
|
||||
export default function fetch<T = any>(
|
||||
event: string,
|
||||
data = {},
|
||||
{ toast = true } = {},
|
||||
): Promise<[string | null, T | null]> {
|
||||
return new Promise((resolve) => {
|
||||
socket.emit(event, data, (res: any) => {
|
||||
if (typeof res === 'string') {
|
||||
if (toast) {
|
||||
Message.error(res);
|
||||
}
|
||||
resolve([res, null]);
|
||||
} else {
|
||||
resolve([null, res]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import HuaJi0 from '@fiora/assets/images/huaji/0.jpg';
|
||||
import HuaJi1 from '@fiora/assets/images/huaji/1.gif';
|
||||
import HuaJi2 from '@fiora/assets/images/huaji/2.jpeg';
|
||||
import HuaJi3 from '@fiora/assets/images/huaji/3.jpeg';
|
||||
import HuaJi4 from '@fiora/assets/images/huaji/4.jpeg';
|
||||
import HuaJi5 from '@fiora/assets/images/huaji/5.jpg';
|
||||
import HuaJi6 from '@fiora/assets/images/huaji/6.jpeg';
|
||||
import HuaJi7 from '@fiora/assets/images/huaji/7.jpg';
|
||||
import HuaJi8 from '@fiora/assets/images/huaji/8.jpeg';
|
||||
import HuaJi9 from '@fiora/assets/images/huaji/9.jpeg';
|
||||
import HuaJi10 from '@fiora/assets/images/huaji/10.jpeg';
|
||||
import HuaJi11 from '@fiora/assets/images/huaji/11.jpeg';
|
||||
import HuaJi12 from '@fiora/assets/images/huaji/12.jpeg';
|
||||
import HuaJi13 from '@fiora/assets/images/huaji/13.jpeg';
|
||||
import HuaJi14 from '@fiora/assets/images/huaji/14.jpeg';
|
||||
import HuaJi15 from '@fiora/assets/images/huaji/15.jpeg';
|
||||
import HuaJi16 from '@fiora/assets/images/huaji/16.jpeg';
|
||||
import HuaJi17 from '@fiora/assets/images/huaji/17.jpeg';
|
||||
import HuaJi18 from '@fiora/assets/images/huaji/18.gif';
|
||||
import HuaJi19 from '@fiora/assets/images/huaji/19.jpeg';
|
||||
import HuaJi20 from '@fiora/assets/images/huaji/20.jpeg';
|
||||
import HuaJi21 from '@fiora/assets/images/huaji/21.jpeg';
|
||||
import HuaJi22 from '@fiora/assets/images/huaji/22.jpeg';
|
||||
import HuaJi23 from '@fiora/assets/images/huaji/23.jpeg';
|
||||
import HuaJi24 from '@fiora/assets/images/huaji/24.jpeg';
|
||||
import HuaJi25 from '@fiora/assets/images/huaji/25.png';
|
||||
import HuaJi26 from '@fiora/assets/images/huaji/26.jpeg';
|
||||
import HuaJi27 from '@fiora/assets/images/huaji/27.jpeg';
|
||||
import HuaJi28 from '@fiora/assets/images/huaji/28.jpeg';
|
||||
import HuaJi29 from '@fiora/assets/images/huaji/29.jpeg';
|
||||
import HuaJi30 from '@fiora/assets/images/huaji/30.jpeg';
|
||||
import HuaJi31 from '@fiora/assets/images/huaji/31.jpeg';
|
||||
import HuaJi32 from '@fiora/assets/images/huaji/32.jpg';
|
||||
import HuaJi33 from '@fiora/assets/images/huaji/33.gif';
|
||||
import HuaJi34 from '@fiora/assets/images/huaji/34.gif';
|
||||
import HuaJi35 from '@fiora/assets/images/huaji/35.gif';
|
||||
import HuaJi36 from '@fiora/assets/images/huaji/36.gif';
|
||||
|
||||
type Huaji = {
|
||||
[key: number]: string;
|
||||
};
|
||||
|
||||
const huaji: Huaji = {
|
||||
0: `${HuaJi0}?width=250&height=250&huaji=true`,
|
||||
1: `${HuaJi1}?width=300&height=300&huaji=true`,
|
||||
2: `${HuaJi2}?width=245&height=206&huaji=true`,
|
||||
3: `${HuaJi3}?width=225&height=225&huaji=true`,
|
||||
4: `${HuaJi4}?width=224&height=225&huaji=true`,
|
||||
5: `${HuaJi5}?width=200&height=200&huaji=true`,
|
||||
6: `${HuaJi6}?width=284&height=177&huaji=true7`,
|
||||
7: `${HuaJi7}?width=300&height=300&huaji=true`,
|
||||
8: `${HuaJi8}?width=225&height=225&huaji=true`,
|
||||
9: `${HuaJi9}?width=204&height=247&huaji=true`,
|
||||
10: `${HuaJi10}?width=223&height=226&huaji=true`,
|
||||
11: `${HuaJi11}?width=198&height=255&huaji=true`,
|
||||
12: `${HuaJi12}?width=212&height=237&huaji=true`,
|
||||
13: `${HuaJi13}?width=290&height=174&huaji=true`,
|
||||
14: `${HuaJi14}?width=224&height=224&huaji=true`,
|
||||
15: `${HuaJi15}?width=224&height=224&huaji=true`,
|
||||
16: `${HuaJi16}?width=225&height=225&huaji=true`,
|
||||
17: `${HuaJi17}?width=225&height=225&huaji=true`,
|
||||
18: `${HuaJi18}?width=100&height=110&huaji=true`,
|
||||
19: `${HuaJi19}?width=225&height=225&huaji=true`,
|
||||
20: `${HuaJi20}?width=225&height=225&huaji=true`,
|
||||
21: `${HuaJi21}?width=225&height=225&huaji=true`,
|
||||
22: `${HuaJi22}?width=245&height=206&huaji=true`,
|
||||
23: `${HuaJi23}?width=225&height=225&huaji=true`,
|
||||
24: `${HuaJi24}?width=225&height=225&huaji=true`,
|
||||
25: `${HuaJi25}?width=225&height=225&huaji=true`,
|
||||
26: `${HuaJi26}?width=225&height=225&huaji=true`,
|
||||
27: `${HuaJi27}?width=180&height=180&huaji=true`,
|
||||
28: `${HuaJi28}?width=235&height=215&huaji=true`,
|
||||
29: `${HuaJi29}?width=278&height=182&huaji=true`,
|
||||
30: `${HuaJi30}?width=228&height=221&huaji=true`,
|
||||
31: `${HuaJi31}?width=239&height=211&huaji=true`,
|
||||
32: `${HuaJi32}?width=220&height=220&huaji=true`,
|
||||
33: `${HuaJi33}?width=220&height=220&huaji=true`,
|
||||
34: `${HuaJi34}?width=164&height=192&huaji=true`,
|
||||
35: `${HuaJi35}?width=130&height=62&huaji=true`,
|
||||
36: `${HuaJi36}?width=187&height=144&huaji=true`,
|
||||
};
|
||||
const HuajiaCount = Object.keys(huaji).length;
|
||||
|
||||
export default function getRandomHuaji() {
|
||||
const number = Math.floor(Math.random() * HuajiaCount);
|
||||
return huaji[number];
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 阻止目标元素下不必要的滚动事件. 解决ios橡皮筋效果问题
|
||||
* @param {HTMLElement} targetElement 目标元素
|
||||
*/
|
||||
export default function inobounce(targetElement: HTMLElement) {
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
function handleTouchStart(e: any) {
|
||||
startY = e.touches ? e.touches[0].screenY : e.screenY;
|
||||
startX = e.touches ? e.touches[0].screenX : e.screenX;
|
||||
}
|
||||
function handleTouchMove(e: any) {
|
||||
let el = e.target;
|
||||
|
||||
while (el !== e.currentTarget) {
|
||||
const style = window.getComputedStyle(el);
|
||||
|
||||
if (!style) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
el.nodeName === 'INPUT' &&
|
||||
el.getAttribute('type') === 'range'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overflowY = style.getPropertyValue('overflow-y');
|
||||
const height = +parseFloat(
|
||||
style.getPropertyValue('height'),
|
||||
).toFixed(0);
|
||||
const isScrollableY =
|
||||
overflowY === 'auto' || overflowY === 'scroll';
|
||||
const canScrollY = el.scrollHeight > el.offsetHeight;
|
||||
if (isScrollableY && canScrollY) {
|
||||
const curY = e.touches ? e.touches[0].screenY : e.screenY;
|
||||
const isAtTop = startY <= curY && el.scrollTop === 0;
|
||||
const isAtBottom =
|
||||
startY >= curY && el.scrollHeight - el.scrollTop === height;
|
||||
|
||||
if (isAtTop || isAtBottom) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const overflowX = style.getPropertyValue('overflow-x');
|
||||
const width = +parseFloat(style.getPropertyValue('width')).toFixed(
|
||||
0,
|
||||
);
|
||||
const isScrollableX =
|
||||
overflowX === 'auto' || overflowX === 'scroll';
|
||||
const canScrollX = el.scrollWidth > el.offsetWidth;
|
||||
if (isScrollableX && canScrollX) {
|
||||
const curX = e.touches ? e.touches[0].screenX : e.screenX;
|
||||
const isAtLeft = startX <= curX && el.scrollLeft === 0;
|
||||
const isAtRight =
|
||||
startX >= curX && el.scrollWidth - el.scrollLeft === width;
|
||||
|
||||
if (isAtLeft || isAtRight) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
el = el.parentNode;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (targetElement) {
|
||||
targetElement.addEventListener('touchstart', handleTouchStart);
|
||||
targetElement.addEventListener('touchmove', handleTouchMove);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export default function notification(
|
||||
title: string,
|
||||
icon: string,
|
||||
body: string,
|
||||
tag = 'tag',
|
||||
duration = 3000,
|
||||
) {
|
||||
if (window.Notification && window.Notification.permission === 'granted') {
|
||||
const n = new window.Notification(title, {
|
||||
icon,
|
||||
body,
|
||||
tag,
|
||||
});
|
||||
n.onclick = function handleClick() {
|
||||
window.focus();
|
||||
this.close();
|
||||
};
|
||||
setTimeout(n.close.bind(n), duration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import DefaultSound from '@fiora/assets/audios/default.mp3';
|
||||
import AppleSound from '@fiora/assets/audios/apple.mp3';
|
||||
import PcQQSound from '@fiora/assets/audios/pcqq.mp3';
|
||||
import MobileQQSound from '@fiora/assets/audios/mobileqq.mp3';
|
||||
import MoMoSound from '@fiora/assets/audios/momo.mp3';
|
||||
import HuaJiSound from '@fiora/assets/audios/huaji.mp3';
|
||||
|
||||
type Sounds = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
const sounds: Sounds = {
|
||||
default: DefaultSound,
|
||||
apple: AppleSound,
|
||||
pcqq: PcQQSound,
|
||||
mobileqq: MobileQQSound,
|
||||
momo: MoMoSound,
|
||||
huaji: HuaJiSound,
|
||||
};
|
||||
|
||||
let prevType = 'default';
|
||||
const $audio = document.createElement('audio');
|
||||
const $source = document.createElement('source');
|
||||
$audio.volume = 0.6;
|
||||
$source.setAttribute('type', 'audio/mp3');
|
||||
$source.setAttribute('src', sounds[prevType]);
|
||||
$audio.appendChild($source);
|
||||
document.body.appendChild($audio);
|
||||
|
||||
let isPlaying = false;
|
||||
|
||||
async function play() {
|
||||
if (!isPlaying) {
|
||||
isPlaying = true;
|
||||
|
||||
try {
|
||||
await $audio.play();
|
||||
} catch (err) {
|
||||
console.warn('播放新消息提示音失败', err.message);
|
||||
} finally {
|
||||
isPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function playSound(type = 'default') {
|
||||
if (type !== prevType) {
|
||||
$source.setAttribute('src', sounds[type]);
|
||||
$audio.load();
|
||||
prevType = type;
|
||||
}
|
||||
play();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
export interface ReadFileResult {
|
||||
/** 文件名 */
|
||||
filename: string;
|
||||
/** 文件拓展名 */
|
||||
ext: string;
|
||||
/** 文件类型 */
|
||||
type: string;
|
||||
/** 文件内容 */
|
||||
result: Blob | ArrayBuffer | string;
|
||||
/** 文件长度 */
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取本地文件
|
||||
* @param {string} resultType 数据类型, {blob|base64}, 默认blob
|
||||
* @param {string} accept 可选文件类型, 默认 * / *
|
||||
*/
|
||||
export default async function readDiskFIle(
|
||||
resultType = 'blob',
|
||||
accept = '*/*',
|
||||
) {
|
||||
const result: ReadFileResult | null = await new Promise((resolve) => {
|
||||
const $input = document.createElement('input');
|
||||
$input.style.display = 'none';
|
||||
$input.setAttribute('type', 'file');
|
||||
$input.setAttribute('accept', accept);
|
||||
// 判断用户是否点击取消, 原生没有提供专门事件, 用hack的方法实现
|
||||
$input.onclick = () => {
|
||||
// @ts-ignore
|
||||
$input.value = null;
|
||||
document.body.onfocus = () => {
|
||||
// onfocus事件会比$input.onchange事件先触发, 因此需要延迟一段时间
|
||||
setTimeout(() => {
|
||||
if ($input.value.length === 0) {
|
||||
resolve(null);
|
||||
}
|
||||
document.body.onfocus = null;
|
||||
}, 500);
|
||||
};
|
||||
};
|
||||
$input.onchange = (e: Event) => {
|
||||
// @ts-ignore
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function handleLoad() {
|
||||
if (!this.result) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
resolve({
|
||||
filename: file.name,
|
||||
ext: file.name
|
||||
.split('.')
|
||||
.pop()
|
||||
.toLowerCase(),
|
||||
type: file.type,
|
||||
// @ts-ignore
|
||||
result: this.result,
|
||||
length:
|
||||
resultType === 'blob'
|
||||
? (this.result as ArrayBuffer).byteLength
|
||||
: (this.result as string).length,
|
||||
});
|
||||
};
|
||||
|
||||
switch (resultType) {
|
||||
case 'blob': {
|
||||
reader.readAsArrayBuffer(file);
|
||||
break;
|
||||
}
|
||||
case 'base64': {
|
||||
reader.readAsDataURL(file);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
$input.click();
|
||||
});
|
||||
|
||||
if (result && resultType === 'blob') {
|
||||
result.result = new Blob(
|
||||
[new Uint8Array(result.result as ArrayBuffer)],
|
||||
{
|
||||
type: result.type,
|
||||
},
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* set global css variable
|
||||
* @param color primary color, three numbers split with comma, like 255,255,255
|
||||
* @param textColor text colore, format like color
|
||||
*/
|
||||
export default function setCssVariable(color: string, textColor: string) {
|
||||
let cssText = '';
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
cssText += `--primary-color-${i}:rgba(${color}, ${i /
|
||||
10});--primary-color-${i}_5:rgba(${color}, ${(i + 0.5) /
|
||||
10});--primary-text-color-${i}:rgba(${textColor}, ${i / 10});`;
|
||||
}
|
||||
document.documentElement.style.cssText += cssText;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import config from '@fiora/config/client';
|
||||
import { sessionID } from '../socket';
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param blob 文件blob数据
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
export default async function uploadFile(blob: Blob): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob);
|
||||
const resp = await fetch(config.server + 'upload_file', {
|
||||
headers: {
|
||||
session_id: sessionID,
|
||||
},
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const result = await resp.json();
|
||||
return result.tempFileName;
|
||||
}
|
||||
Reference in New Issue
Block a user