Skip to content
This repository has been archived by the owner on Jul 25, 2023. It is now read-only.

Commit

Permalink
release 1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
FlysoftBeta committed Jul 20, 2023
1 parent 0c3ac73 commit c0a6eb2
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 94 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = true
end_of_line = lf
insert_final_newline = true

[*.{js,json,yml}]
[*.{ts,tsx,css,json,yml}]
charset = utf-8
indent_style = space
indent_size = 4
Binary file added .github/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# QQNTim 模板插件
# QQNTim 消息增强

## 简介

本插件为 QQNT 提供复读消息、转发语音消息、撤回好友私聊消息等功能。需要安装 [QQNTim 3.0 及以上版本](https://github.com/Flysoft-Studio/QQNTim) 才能使用。

效果图:

![截图](.github/screenshot.png)

## 使用

安装此插件后对着消息右键即可看到添加的功能。

## 开发

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "qqntim-plugin-template",
"name": "qqntim-plugin-enhanced-messaging",
"private": true,
"version": "2.0.0",
"version": "1.0.0",
"packageManager": "[email protected]",
"license": "MIT",
"scripts": {
Expand Down
6 changes: 3 additions & 3 deletions publish/qqntim.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"manifestVersion": "3.0",
"id": "template-plugin",
"name": "QQNTim 模板插件",
"description": "快速开始开发你自己的插件",
"id": "enhanced-messaging",
"name": "消息增强",
"description": "提供复读消息、转发语音消息、撤回好友私聊消息等功能",
"version": "1.0.0",
"author": "Flysoft",
"injections": [
Expand Down
13 changes: 12 additions & 1 deletion publish/style.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
/* Put your stylesheet here. */
.enhanced-messaging-floating-ok {
position: fixed;
bottom: 30px;
right: 120px;
z-index: 10000;
}

.enhanced-messaging-floating-cancel {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 10000;
}
218 changes: 218 additions & 0 deletions src/_renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { dialog, env, nt, utils } from "qqntim/renderer";
import { getPluginConfig } from "./config";
import { createRoot } from "react-dom/client";

function selectPeer() {
return new Promise<Map<string, QQNTim.API.Renderer.NT.Peer> | undefined>((resolve) => {
const currentSelected = document.querySelector<HTMLElement>(".recent-contact-item.recent-contact-item--selected");
currentSelected?.classList.remove("list-item--selected", "recent-contact-item--selected");
const selectedPeers = new Map<string, QQNTim.API.Renderer.NT.Peer>();
const mouseDownHandler = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
};
const clickHandler = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const element = (event.target as HTMLElement | undefined)?.closest?.<HTMLElement>(".recent-contact-item");
const itemData = element?.__VUE__?.[0]?.props?.itemData;
if (itemData)
if (!selectedPeers.has(itemData.uid)) {
selectedPeers.set(itemData.uid, { chatType: itemData.type == 1 ? "friend" : itemData.type == 2 ? "group" : "others", uid: itemData.uid });
element?.classList.add("list-item--selected", "recent-contact-item--selected");
} else {
selectedPeers.delete(itemData.uid);
element?.classList.remove("list-item--selected", "recent-contact-item--selected");
}
};
const elements = document.querySelectorAll<HTMLElement>(".recent-contact-item");
for (const element of elements) {
(element?.firstElementChild as HTMLElement | null)?.addEventListener("mousedown", mouseDownHandler);
(element?.firstElementChild as HTMLElement | null)?.addEventListener("click", clickHandler);
}
const exitSelect = () => {
for (const element of elements) {
(element?.firstElementChild as HTMLElement | null)?.removeEventListener("mousedown", mouseDownHandler);
(element?.firstElementChild as HTMLElement | null)?.removeEventListener("click", clickHandler);
element.classList.remove("list-item--selected", "recent-contact-item--selected");
}
currentSelected?.classList.add("list-item--selected", "recent-contact-item--selected");
okBtn.remove();
cancelBtn.remove();
};
const okBtn = document.createElement("button");
okBtn.classList.add("q-button", "q-button--default", "q-button--primary", "enhanced-messaging-floating-ok");
const okBtnText = document.createElement("span");
okBtnText.classList.add("q-button__slot-warp");
okBtnText.innerText = "确定";
okBtn.appendChild(okBtnText);
okBtn.addEventListener("click", () => {
exitSelect();
resolve(selectedPeers);
});
const cancelBtn = document.createElement("button");
cancelBtn.classList.add("q-button", "q-button--default", "q-button--secondary", "enhanced-messaging-floating-cancel");
const cancelBtnText = document.createElement("span");
cancelBtnText.classList.add("q-button__slot-warp");
cancelBtnText.innerText = "取消";
cancelBtn.appendChild(cancelBtnText);
cancelBtn.addEventListener("click", () => {
exitSelect();
resolve(undefined);
});
document.body.appendChild(okBtn);
document.body.appendChild(cancelBtn);
});
}

function encodeMsg(msg: any) {
return msg.elements.map((_element) => {
const element = JSON.parse(JSON.stringify(_element));
return {
type: "raw",
raw: {
...element,
elementId: "",
extBufForUI: undefined,
picElement: element.picElement && {
...element.picElement,
fileSubId: undefined,
fileUuid: undefined,
progress: undefined,
thumbPath: undefined,
transferStatus: undefined,
},
},
};
});
}

function forwardMsg(msg: any) {
selectPeer().then((peers) => {
if (!peers) return;
Promise.all(Array.from(peers.values()).map((peer) => nt.sendMessage(peer, encodeMsg(msg)).catch((reason) => console.error(reason))))
.then((array) => array.filter(Boolean))
.then((array) => {
dialog.alert(`成功向 ${array.length} 个对象转发此消息。`);
});
});
}

function repeatMsg(msg: any) {
nt.sendMessage({ uid: msg.peerUid, chatType: msg.chatType == 1 ? "friend" : msg.chatType == 2 ? "group" : "others" }, encodeMsg(msg));
}

function forceRevokeMsg(msg: any) {
nt.revokeMessage({ uid: msg.peerUid, chatType: msg.chatType == 1 ? "friend" : msg.chatType == 2 ? "group" : "others" }, msg.msgId);
}

function ShareIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M6 2H3C2.44772 2 2 2.44772 2 3V13C2 13.5523 2.44772 14 3 14H13C13.5523 14 14 13.5523 14 13V10H13V13H3L3 3H6V2Z" fill="currentColor" />
<path d="M8 4.5H13V3.5H8V4.5ZM7.5 10V5H6.5V10H7.5ZM8 3.5C7.17157 3.5 6.5 4.17157 6.5 5H7.5C7.5 4.72386 7.72386 4.5 8 4.5V3.5Z" fill="currentColor" />
<path d="M11.1213 1.87868L13.2426 4L11.1213 6.12132" stroke="currentColor" />
</svg>
);
}

function RevokeIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4.5H9.5C11.7091 4.5 13.5 6.29086 13.5 8.5V8.5C13.5 10.7091 11.7091 12.5 9.5 12.5H3" stroke="currentColor" />
<path d="M5.5 1.5L2.50004 4.49998L5.5 7.49997" stroke="currentColor" strokeLinejoin="round" />
</svg>
);
}

function Menu({ msg, showForwardBtn, showPlusOneBtn, showForceRevokeBtn, blur }: { msg: any; showForwardBtn: boolean; showPlusOneBtn: boolean; showForceRevokeBtn: boolean; blur: Function }) {
return (
<>
{(showForwardBtn || showPlusOneBtn) && <div className="q-context-menu-separator" role="separator" />}
{([showForwardBtn && ["转发", <ShareIcon />, () => forwardMsg(msg)], showForceRevokeBtn && ["强制撤回", <RevokeIcon />, () => forceRevokeMsg(msg)], showPlusOneBtn && ["+1", undefined, () => repeatMsg(msg)]].filter(Boolean) as [string, React.ReactNode, Function][]).map(
([title, icon, onClick], idx) => (
<a
// rome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={idx}
className="q-context-menu-item q-context-menu-item--normal"
role="item"
onClick={() => {
onClick();
blur();
}}
>
<i className="q-icon q-context-menu-item__head" style={{ width: "16px", height: "16px", alignItems: "center", color: "inherit", display: "inline-flex", justifyContent: "center" }}>
{icon}
</i>
<span className="q-context-menu-item__text">{title}</span>
</a>
),
)}
</>
);
}

export default class Entry implements QQNTim.Entry.Renderer {
constructor() {
const config = getPluginConfig(env.config.plugins.config);
}

onWindowLoaded(): void {
const handler = (event: MouseEvent) => {
const msg = (event.target as HTMLElement | undefined)?.closest(".msg-content-container")?.closest<HTMLElement>(".message")?.__VUE__?.[0]?.props?.msgRecord;
let showForwardBtn = true;
let showForceRevokeBtn = msg.chatType == 1;
if (msg) {
utils.waitForElement<HTMLElement>(".q-context-menu").then((menu) => {
const items = document.querySelectorAll<HTMLElement>(".q-context-menu .q-context-menu-item .q-context-menu-item__text");
for (const item of items) {
if (item.innerText == "转发") showForwardBtn = false;
if (item.innerText == "撤回") showForceRevokeBtn = false;
}
const pluginMenuElement = document.createElement("div");
const pluginMenuRoot = createRoot(pluginMenuElement);
pluginMenuRoot.render(
<Menu
msg={msg}
showForwardBtn={showForwardBtn}
showForceRevokeBtn={showForceRevokeBtn}
showPlusOneBtn={true}
blur={() => {
menu.blur();
const mouseEvent = new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
});
document.querySelector<HTMLElement>("#app")?.dispatchEvent(mouseEvent);
}}
/>,
);
menu.appendChild(pluginMenuElement);
requestAnimationFrame(() => {
const rect = menu.getBoundingClientRect();
if (rect.bottom + 20 > document.body.clientHeight) {
menu.style.top = "";
menu.style.bottom = "20px";
}
if (rect.right + 20 > document.body.clientWidth) {
menu.style.left = "";
menu.style.right = "20px";
}
});
});
}
};
window.addEventListener("contextmenu", handler);
// 由于图片比较特殊,会阻止右键消息传达到 window,所以需要单独设置 listener
new MutationObserver(() => {
const elements = document.querySelectorAll<HTMLElement>(".image-content:not(.enhanced-messaging-img-patched)");
for (const element of elements) {
element.classList.add("enhanced-messaging-img-patched");
element.addEventListener("contextmenu", handler);
}
}).observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
}
}
16 changes: 3 additions & 13 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
export const id = "my-template-plugin" as const;
export const id = "enhanced-messaging" as const;

export const defaults: PluginConfig = {
switchConfigItem: false,
anotherSwitchConfigItem: false,
inputConfigItem: "默认值",
dropdownConfigItem: "A",
};
export const defaults: PluginConfig = {};
export function getPluginConfig(config: Config | undefined) {
return Object.assign({}, defaults, config?.[id] || {});
}

export interface PluginConfig {
switchConfigItem: boolean;
anotherSwitchConfigItem: boolean;
inputConfigItem: string;
dropdownConfigItem: "A" | "B" | "C";
}
export type PluginConfig = {};
export type Config = {
[X in typeof id]?: Partial<PluginConfig>;
};
11 changes: 1 addition & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
import { getPluginConfig } from "./config";
import * as qqntim from "qqntim/main";

export default class Entry implements QQNTim.Entry.Main {
constructor() {
const config = getPluginConfig(qqntim.env.config.plugins.config);
console.log("[Template] Hello world!", qqntim);
console.log("[Template] 当前插件配置:", config);
}
}
export default class Entry implements QQNTim.Entry.Main {}
12 changes: 2 additions & 10 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
import { getPluginConfig } from "./config";
import * as qqntim from "qqntim/renderer";

export default class Entry implements QQNTim.Entry.Renderer {
constructor() {
const config = getPluginConfig(qqntim.env.config.plugins.config);
console.log("[Template] Hello world!", qqntim);
console.log("[Template] 当前插件配置:", config);
}
}
import Entry from "./_renderer";
export default Entry;
52 changes: 1 addition & 51 deletions src/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1 @@
import { usePluginConfig } from "./utils/hooks";
import { defineSettingsPanels } from "qqntim-settings";
import { Dropdown, Input, SettingsBox, SettingsBoxItem, SettingsSection, Switch } from "qqntim-settings/components";
import { env } from "qqntim/renderer";
import { useMemo } from "react";
import { getPluginConfig } from "./config";

export default class Entry implements QQNTim.Entry.Renderer {
constructor() {
// 如果不需要设置界面,将下一行注释掉即可;如果需要在设置项目旁边加一个小图标,请将 `undefined` 改为一段 HTML 代码(可以是 `<svg>`, `<img>` 等等)。
defineSettingsPanels(["模板插件设置", SettingsPanel, undefined]);
}
}

function SettingsPanel({ config: _config, setConfig: _setConfig }: QQNTim.Settings.PanelProps) {
const [pluginConfig, setPluginConfig] = usePluginConfig(_config, _setConfig);
const currentPluginConfigString = useMemo(() => JSON.stringify(getPluginConfig(env.config.plugins.config)), []);

return (
<>
<SettingsSection title="插件设置">
<SettingsBox>
<SettingsBoxItem title="当前生效的插件配置:" description={[currentPluginConfigString]} />
<SettingsBoxItem title="开关" description={["这是一个开关。", `当前状态为:${pluginConfig.switchConfigItem ? "开" : "关"}`]}>
<Switch checked={pluginConfig.switchConfigItem} onToggle={(state) => setPluginConfig("switchConfigItem", state)} />
</SettingsBoxItem>
{pluginConfig.switchConfigItem && (
<SettingsBoxItem title="另一个开关" description={["这是另一个开关。", `当前状态为:${pluginConfig.anotherSwitchConfigItem ? "开" : "关"}`]}>
<Switch checked={pluginConfig.anotherSwitchConfigItem} onToggle={(state) => setPluginConfig("anotherSwitchConfigItem", state)} />
</SettingsBoxItem>
)}
<SettingsBoxItem title="下拉菜单" description={["这是一个下拉菜单。", `当前状态为:${pluginConfig.dropdownConfigItem}`]}>
<Dropdown
items={[
["A" as const, "我是 A 选项"],
["B" as const, "我是 B 选项"],
["C" as const, "我是 C 选项"],
]}
selected={pluginConfig.dropdownConfigItem}
onChange={(state) => setPluginConfig("dropdownConfigItem", state)}
width="150px"
/>
</SettingsBoxItem>
<SettingsBoxItem title="输入框" description={["这是一个输入框。", `当前状态为:${pluginConfig.inputConfigItem}`]} isLast={true}>
<Input value={pluginConfig.inputConfigItem} onChange={(state) => setPluginConfig("inputConfigItem", state)} />
</SettingsBoxItem>
</SettingsBox>
</SettingsSection>
</>
);
}
export default class Entry implements QQNTim.Entry.Renderer {}
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1277,9 +1277,9 @@ __metadata:
languageName: node
linkType: hard

"qqntim-plugin-template@workspace:.":
"qqntim-plugin-enhanced-messaging@workspace:.":
version: 0.0.0-use.local
resolution: "qqntim-plugin-template@workspace:."
resolution: "qqntim-plugin-enhanced-messaging@workspace:."
dependencies:
"@flysoftbeta/qqntim-typings": ^3.1.2
"@types/node": ^20.4.2
Expand Down

0 comments on commit c0a6eb2

Please sign in to comment.