mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-08 08:42:17 +02:00
Favorites network
This commit is contained in:
parent
9dbb6e5e19
commit
f2a8d5aacc
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<ChannelWrapper ref="wrapper" v-bind="$props">
|
||||
<span class="name">{{ channel.name }}</span>
|
||||
<span class="name">{{ name() }}</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{highlight: channel.highlight && !channel.muted}"
|
||||
|
@ -51,6 +51,9 @@ export default {
|
|||
close() {
|
||||
this.$root.closeChannel(this.channel);
|
||||
},
|
||||
name() {
|
||||
return this.channel.displayName ? this.channel.displayName : this.channel.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
27
client/components/CollapseButton.css
Normal file
27
client/components/CollapseButton.css
Normal file
|
@ -0,0 +1,27 @@
|
|||
.collapse-network {
|
||||
width: 40px;
|
||||
opacity: 0.4;
|
||||
padding-left: 11px;
|
||||
transition: opacity 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-network-icon {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.network.collapsed .collapse-network-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.collapse-network-icon::before {
|
||||
content: "\f0d7"; /* http://fontawesome.io/icon/caret-down/ */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.collapse-network:hover {
|
||||
opacity: 1;
|
||||
}
|
36
client/components/CollapseFavoritesButton.vue
Normal file
36
client/components/CollapseFavoritesButton.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="favorites.length > 0"
|
||||
:aria-label="getExpandLabel()"
|
||||
:aria-expanded="isCollapsed"
|
||||
class="collapse-network"
|
||||
@click.stop="onCollapseClick"
|
||||
>
|
||||
<span class="collapse-network-icon" />
|
||||
</button>
|
||||
<span v-else class="collapse-network" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "./CollapseButton.css";
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CollapseFavoritesButton",
|
||||
props: {
|
||||
onCollapseClick: Function,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
favorites: this.$store.state.favoriteChannels,
|
||||
isCollapsed: !this.$store.state.favoritesOpen,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getExpandLabel() {
|
||||
return this.isCollapsed ? "Expand" : "Collapse";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
32
client/components/CollapseNetworkButton.vue
Normal file
32
client/components/CollapseNetworkButton.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="network.channels.length > 1"
|
||||
:aria-controls="'network-' + network.uuid"
|
||||
:aria-label="getExpandLabel(network)"
|
||||
:aria-expanded="!network.isCollapsed"
|
||||
class="collapse-network"
|
||||
@click.stop="onCollapseClick"
|
||||
>
|
||||
<span class="collapse-network-icon" />
|
||||
</button>
|
||||
<span v-else class="collapse-network" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "./CollapseButton.css";
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CollapseNetworkButton",
|
||||
props: {
|
||||
network: Object,
|
||||
onCollapseClick: Function,
|
||||
},
|
||||
methods: {
|
||||
getExpandLabel(network) {
|
||||
return network.isCollapsed ? "Expand" : "Collapse";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -43,6 +43,7 @@ import {
|
|||
generateUserContextMenu,
|
||||
generateChannelContextMenu,
|
||||
generateInlineChannelContextMenu,
|
||||
generateFavoritesContextMenu,
|
||||
} from "../js/helpers/contextMenu.js";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
|
@ -70,6 +71,7 @@ export default {
|
|||
eventbus.on("contextmenu:user", this.openUserContextMenu);
|
||||
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
|
||||
eventbus.on("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
||||
eventbus.on("contextmenu:favorites", this.openFavoritesContextMenu);
|
||||
},
|
||||
destroyed() {
|
||||
eventbus.off("escapekey", this.close);
|
||||
|
@ -77,6 +79,7 @@ export default {
|
|||
eventbus.off("contextmenu:user", this.openUserContextMenu);
|
||||
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
|
||||
eventbus.off("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
||||
eventbus.off("contextmenu:favorites", this.openFavoritesContextMenu);
|
||||
|
||||
this.close();
|
||||
},
|
||||
|
@ -119,6 +122,10 @@ export default {
|
|||
);
|
||||
this.open(data.event, items);
|
||||
},
|
||||
openFavoritesContextMenu(data) {
|
||||
const items = generateFavoritesContextMenu();
|
||||
this.open(data.event, items);
|
||||
},
|
||||
open(event, items) {
|
||||
event.preventDefault();
|
||||
|
||||
|
|
75
client/components/Favorites.vue
Normal file
75
client/components/Favorites.vue
Normal file
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="favorites">
|
||||
<div class="channel-list-item" data-type="lobby" @contextmenu.prevent="openContextMenu">
|
||||
<div class="lobby-wrap">
|
||||
<CollapseFavoritesButton :on-collapse-click="onCollapseClick" />
|
||||
<span title="Favorites" class="name">Favorites</span>
|
||||
<span v-if="unreadCount > 0" class="badge">{{ unreadCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="channel in $store.state.favoriteChannels" :key="channel.id">
|
||||
<Channel
|
||||
:channel="channel"
|
||||
:network="network"
|
||||
:is-filtering="false"
|
||||
:active="
|
||||
$store.state.activeChannel && channel === $store.state.activeChannel.channel
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lobby-wrap {
|
||||
display: flex;
|
||||
/* margin-left: 40px; */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import eventbus from "../js/eventbus";
|
||||
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
||||
import Channel from "./Channel.vue";
|
||||
import CollapseFavoritesButton from "./CollapseFavoritesButton.vue";
|
||||
|
||||
export default {
|
||||
name: "Favorites",
|
||||
components: {
|
||||
Channel,
|
||||
CollapseFavoritesButton,
|
||||
},
|
||||
props: {
|
||||
channels: Array,
|
||||
},
|
||||
computed: {
|
||||
network() {
|
||||
return {
|
||||
isCollapsed: !this.$store.state.favoritesOpen,
|
||||
status: {
|
||||
connected: true,
|
||||
secure: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCollapseClick() {
|
||||
this.$store.commit("toggleFavorites");
|
||||
},
|
||||
openContextMenu(event) {
|
||||
eventbus.emit("contextmenu:favorites", {
|
||||
event: event,
|
||||
channel: this.channel,
|
||||
});
|
||||
},
|
||||
unreadCount() {
|
||||
const unread = this.channels.reduce((acc, channel) => {
|
||||
return acc + channel.unread || 0;
|
||||
}, 0);
|
||||
|
||||
return roundBadgeNumber(unread);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -69,6 +69,19 @@
|
|||
@choose="onDraggableChoose"
|
||||
@unchoose="onDraggableUnchoose"
|
||||
>
|
||||
<div
|
||||
v-if="$store.state.favoriteChannels.length"
|
||||
id="favorites"
|
||||
aria-label="Favorite channels"
|
||||
class="network"
|
||||
:class="{
|
||||
collapsed: !$store.state.favoritesOpen,
|
||||
}"
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Favorites :channels="$store.state.favoriteChannels" />
|
||||
</div>
|
||||
<div
|
||||
v-for="network in $store.state.networks"
|
||||
:id="'network-' + network.uuid"
|
||||
|
@ -101,7 +114,6 @@
|
|||
:channel="network.channels[0]"
|
||||
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||
/>
|
||||
|
||||
<Draggable
|
||||
draggable=".channel-list-item"
|
||||
ghost-class="ui-sortable-ghost"
|
||||
|
@ -118,7 +130,7 @@
|
|||
>
|
||||
<template v-for="(channel, index) in network.channels">
|
||||
<Channel
|
||||
v-if="index > 0"
|
||||
v-if="index > 0 && !channel.favorite"
|
||||
:key="channel.id"
|
||||
:channel="channel"
|
||||
:network="network"
|
||||
|
@ -200,6 +212,7 @@ import Mousetrap from "mousetrap";
|
|||
import Draggable from "vuedraggable";
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import NetworkLobby from "./NetworkLobby.vue";
|
||||
import Favorites from "./Favorites.vue";
|
||||
import Channel from "./Channel.vue";
|
||||
import JoinChannel from "./JoinChannel.vue";
|
||||
|
||||
|
@ -216,6 +229,7 @@ export default {
|
|||
NetworkLobby,
|
||||
Channel,
|
||||
Draggable,
|
||||
Favorites,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -263,6 +277,8 @@ export default {
|
|||
Mousetrap.bind("alt+shift+right", this.expandNetwork);
|
||||
Mousetrap.bind("alt+shift+left", this.collapseNetwork);
|
||||
Mousetrap.bind("alt+j", this.toggleSearch);
|
||||
|
||||
console.log(this.$store.state.favoriteChannels[0]);
|
||||
},
|
||||
beforeDestroy() {
|
||||
Mousetrap.unbind("alt+shift+right", this.expandNetwork);
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
<template>
|
||||
<ChannelWrapper v-bind="$props" :channel="channel">
|
||||
<button
|
||||
v-if="network.channels.length > 1"
|
||||
:aria-controls="'network-' + network.uuid"
|
||||
:aria-label="getExpandLabel(network)"
|
||||
:aria-expanded="!network.isCollapsed"
|
||||
class="collapse-network"
|
||||
@click.stop="onCollapseClick"
|
||||
>
|
||||
<span class="collapse-network-icon" />
|
||||
</button>
|
||||
<span v-else class="collapse-network" />
|
||||
<CollapseNetworkButton :network="network" :on-collapse-click="onCollapseClick" />
|
||||
<div class="lobby-wrap">
|
||||
<span :title="channel.name" class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
|
@ -49,11 +39,13 @@
|
|||
import collapseNetwork from "../js/helpers/collapseNetwork";
|
||||
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
||||
import ChannelWrapper from "./ChannelWrapper.vue";
|
||||
import CollapseNetworkButton from "./CollapseNetworkButton.vue";
|
||||
|
||||
export default {
|
||||
name: "Channel",
|
||||
components: {
|
||||
ChannelWrapper,
|
||||
CollapseNetworkButton,
|
||||
},
|
||||
props: {
|
||||
network: Object,
|
||||
|
@ -76,9 +68,6 @@ export default {
|
|||
onCollapseClick() {
|
||||
collapseNetwork(this.network, !this.network.isCollapsed);
|
||||
},
|
||||
getExpandLabel(network) {
|
||||
return network.isCollapsed ? "Expand" : "Collapse";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -369,6 +369,7 @@ p {
|
|||
.context-menu-action-revoke-mode::before { content: "\f068"; /* http://fontawesome.io/icon/minus/ */ }
|
||||
.context-menu-network::before { content: "\f233"; /* https://fontawesome.com/icons/server?style=solid */ }
|
||||
.context-menu-edit::before { content: "\f303"; /* https://fontawesome.com/icons/pencil-alt?style=solid */ }
|
||||
.context-menu-clear-favorites::before,
|
||||
.context-menu-clear-history::before { content: "\f1f8"; /* https://fontawesome.com/icons/trash?style=solid */ }
|
||||
.context-menu-mute::before { content: "\f6a9"; /* https://fontawesome.com/v5.15/icons/volume-mute?style=solid */ }
|
||||
|
||||
|
@ -919,34 +920,6 @@ background on hover (unless active) */
|
|||
transform: rotate(45deg) translateZ(0);
|
||||
}
|
||||
|
||||
#sidebar .network .collapse-network {
|
||||
width: 40px;
|
||||
opacity: 0.4;
|
||||
padding-left: 11px;
|
||||
transition: opacity 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#sidebar .network .collapse-network-icon {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
#sidebar .network.collapsed .collapse-network-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
#sidebar .network .collapse-network-icon::before {
|
||||
content: "\f0d7"; /* http://fontawesome.io/icon/caret-down/ */
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#sidebar .collapse-network:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#footer {
|
||||
height: 45px;
|
||||
font-size: 14px;
|
||||
|
|
33
client/js/commands/favorite.js
Normal file
33
client/js/commands/favorite.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
"use strict";
|
||||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
|
||||
function input(args) {
|
||||
if (args.length === 0) {
|
||||
const {channel} = store.state.activeChannel;
|
||||
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: `/favorite ${channel.name}`,
|
||||
});
|
||||
} else {
|
||||
for (const arg of args) {
|
||||
for (const network of store.state.networks) {
|
||||
const channel = network.channels.find((c) => c.name === arg);
|
||||
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
socket.emit("input", {
|
||||
target: channel.id,
|
||||
text: `/favorite ${channel.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {input};
|
|
@ -88,6 +88,21 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||
}),
|
||||
},
|
||||
];
|
||||
// Add menu items for all except lobbies
|
||||
} else {
|
||||
// Add favorites item
|
||||
items.push({
|
||||
label: channel.favorite ? "Remove from favorites" : "Add to favorites",
|
||||
type: "item",
|
||||
class: "favorite",
|
||||
action() {
|
||||
if (channel.favorite) {
|
||||
socket.emit("favorites:remove", channel.id);
|
||||
} else {
|
||||
socket.emit("favorites:add", channel.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add menu items for channels
|
||||
|
@ -208,6 +223,21 @@ export function generateChannelContextMenu($root, channel, network) {
|
|||
return items;
|
||||
}
|
||||
|
||||
export function generateFavoritesContextMenu() {
|
||||
const items = [];
|
||||
|
||||
items.push({
|
||||
label: "Clear favorites",
|
||||
type: "item",
|
||||
class: "clear-favorites",
|
||||
action() {
|
||||
socket.emit("favorites:clear");
|
||||
},
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function generateInlineChannelContextMenu($root, chan, network) {
|
||||
const join = () => {
|
||||
const channel = network.channels.find((c) => c.name === chan);
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
import store from "../store";
|
||||
|
||||
export default (network, channel) => {
|
||||
if (!network.isCollapsed || channel.highlight || channel.type === "lobby") {
|
||||
if (
|
||||
!network.isCollapsed ||
|
||||
channel.highlight ||
|
||||
channel.type === "lobby" ||
|
||||
(channel.favorite === true && store.state.favoritesOpen)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
10
client/js/socket-events/favorites.js
Normal file
10
client/js/socket-events/favorites.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
"use strict";
|
||||
|
||||
import socket from "../socket";
|
||||
import store from "../store";
|
||||
|
||||
socket.on("favorites", function (data) {
|
||||
console.log("favorites", data);
|
||||
|
||||
store.commit("favoriteChannels", data.favoriteChannels);
|
||||
});
|
|
@ -27,3 +27,4 @@ import "./history_clear";
|
|||
import "./mentions";
|
||||
import "./search";
|
||||
import "./mute_changed";
|
||||
import "./favorites";
|
||||
|
|
|
@ -11,6 +11,7 @@ socket.on("init", function (data) {
|
|||
store.commit("networks", mergeNetworkData(data.networks));
|
||||
store.commit("isConnected", true);
|
||||
store.commit("currentUserVisibleError", null);
|
||||
store.commit("favoriteChannels", data.favoriteChannels);
|
||||
|
||||
if (data.token) {
|
||||
storage.set("token", data.token);
|
||||
|
|
|
@ -28,6 +28,7 @@ const store = new Vuex.Store({
|
|||
isAutoCompleting: false,
|
||||
isConnected: false,
|
||||
networks: [],
|
||||
favoriteChannels: [],
|
||||
mentions: [],
|
||||
hasServiceWorker: false,
|
||||
pushNotificationState: "unsupported",
|
||||
|
@ -43,6 +44,7 @@ const store = new Vuex.Store({
|
|||
messageSearchResults: null,
|
||||
messageSearchInProgress: false,
|
||||
searchEnabled: false,
|
||||
favoritesOpen: storage.get("thelounge.state.favorites") !== "false",
|
||||
},
|
||||
mutations: {
|
||||
appLoaded(state) {
|
||||
|
@ -129,10 +131,59 @@ const store = new Vuex.Store({
|
|||
|
||||
state.messageSearchResults = value;
|
||||
},
|
||||
favoriteChannels(state, payload) {
|
||||
console.log("payload", payload);
|
||||
state.favoriteChannels.forEach((channel) => {
|
||||
channel.favorite = false;
|
||||
channel.displayName = "";
|
||||
});
|
||||
|
||||
store.favoriteChannels = [];
|
||||
|
||||
// Channels can have the same name across networks, so we need to track and distinguish duplicates.
|
||||
// We use a map so we can go back and update the channel that the name is a duplicate of.
|
||||
// If they have a the same names on two different networks that have the same name,
|
||||
// that's on them. I'm not paid to do this.
|
||||
const names = new Map(); // Map of name --> { channelId, networkuuId }
|
||||
state.favoriteChannels = payload.map(({channelId, networkUuid}) => {
|
||||
const netChan = this.getters.findChannelOnNetworkById(networkUuid, channelId);
|
||||
netChan.channel.favorite = true;
|
||||
|
||||
if (names.has(netChan.channel.name)) {
|
||||
const dupe = names.get(netChan.channel.name);
|
||||
|
||||
if (dupe) {
|
||||
const otherNetChan = this.getters.findChannelOnNetworkById(
|
||||
dupe.networkId,
|
||||
dupe.channelId
|
||||
);
|
||||
|
||||
netChan.channel.displayName =
|
||||
netChan.channel.name + `(${netChan.network.name})`;
|
||||
|
||||
otherNetChan.channel.displayName =
|
||||
otherNetChan.channel.name + ` (${otherNetChan.network.name})`;
|
||||
}
|
||||
} else {
|
||||
names.set(netChan.channel.name, {
|
||||
channelId: netChan.channel.id,
|
||||
networkId: netChan.network.uuid,
|
||||
});
|
||||
}
|
||||
|
||||
return netChan.channel;
|
||||
});
|
||||
},
|
||||
toggleFavorites(state) {
|
||||
state.favoritesOpen = !state.favoritesOpen;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
partChannel({commit, state}, netChan) {
|
||||
const mentions = state.mentions.filter((msg) => !(msg.chanId === netChan.channel.id));
|
||||
const favorites = state.favoriteChannels.filter((fav) => fav.id !== netChan.channel.id);
|
||||
|
||||
commit("favoriteChannels", favorites);
|
||||
commit("mentions", mentions);
|
||||
},
|
||||
},
|
||||
|
@ -156,6 +207,21 @@ const store = new Vuex.Store({
|
|||
|
||||
return null;
|
||||
},
|
||||
findChannelOnNetworkById: (state) => (networkUuid, channelId) => {
|
||||
for (const network of state.networks) {
|
||||
if (network.uuid !== networkUuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const channel of network.channels) {
|
||||
if (channel.id === channelId) {
|
||||
return {network, channel};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
findChannel: (state) => (id) => {
|
||||
for (const network of state.networks) {
|
||||
for (const channel of network.channels) {
|
||||
|
|
|
@ -89,6 +89,13 @@ store.watch(
|
|||
}
|
||||
);
|
||||
|
||||
store.watch(
|
||||
(state) => state.favoritesOpen,
|
||||
(favoritesOpen) => {
|
||||
storage.set("thelounge.state.favorites", favoritesOpen);
|
||||
}
|
||||
);
|
||||
|
||||
store.watch(
|
||||
(_, getters) => getters.title,
|
||||
(title) => {
|
||||
|
|
|
@ -59,6 +59,7 @@ function Client(manager, name, config = {}) {
|
|||
idMsg: 1,
|
||||
name: name,
|
||||
networks: [],
|
||||
favoriteChannels: [],
|
||||
mentions: [],
|
||||
manager: manager,
|
||||
messageStorage: [],
|
||||
|
@ -119,7 +120,18 @@ function Client(manager, name, config = {}) {
|
|||
}
|
||||
});
|
||||
|
||||
(client.config.networks || []).forEach((network) => client.connect(network, true));
|
||||
(client.config.networks || []).forEach((network) => {
|
||||
client.connect(network, true);
|
||||
|
||||
for (const chan of network.channels) {
|
||||
if (chan.favorite) {
|
||||
// third argument is whether to save or not;
|
||||
// we don't need to here as the config is loaded from the filesystem
|
||||
console.log(network.uuid, chan.id);
|
||||
client.addToFavorites(network.uuid, chan.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Networks are stored directly in the client object
|
||||
// We don't need to keep it in the config object
|
||||
|
@ -654,6 +666,7 @@ Client.prototype.part = function (network, chan) {
|
|||
const client = this;
|
||||
network.channels = _.without(network.channels, chan);
|
||||
client.mentions = client.mentions.filter((msg) => !(msg.chanId === chan.id));
|
||||
client.favoriteChannels = client.favoriteChannels.filter((fav) => fav.id !== chan.id);
|
||||
chan.destroy();
|
||||
client.save();
|
||||
client.emit("part", {
|
||||
|
@ -769,3 +782,81 @@ Client.prototype.save = _.debounce(
|
|||
5000,
|
||||
{maxWait: 20000}
|
||||
);
|
||||
|
||||
Client.prototype.addToFavorites = function (networkUuid, chanId, shouldSave = true) {
|
||||
const client = this;
|
||||
const favorites = client.favoriteChannels;
|
||||
const isFavorited = favorites.find(({channelId}) => channelId === chanId);
|
||||
|
||||
if (!isFavorited) {
|
||||
favorites.push({
|
||||
channelId: chanId,
|
||||
networkUuid: networkUuid,
|
||||
});
|
||||
|
||||
if (shouldSave) {
|
||||
client.save();
|
||||
}
|
||||
}
|
||||
|
||||
client.emitToAttachedClients("favorites", {
|
||||
favoriteChannels: client.favoriteChannels,
|
||||
});
|
||||
|
||||
const netChan = client.find(chanId);
|
||||
|
||||
if (netChan.chan) {
|
||||
netChan.chan.favorite = true;
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.removeFromFavorites = function (chanId) {
|
||||
const client = this;
|
||||
const favorites = client.favoriteChannels;
|
||||
const isFavorited = favorites.find(({channelId}) => channelId === chanId);
|
||||
|
||||
if (isFavorited) {
|
||||
favorites.splice(favorites.indexOf(isFavorited), 1);
|
||||
client.save();
|
||||
}
|
||||
|
||||
client.emitToAttachedClients("favorites", {
|
||||
favoriteChannels: client.favoriteChannels,
|
||||
});
|
||||
|
||||
const netChan = client.find(chanId);
|
||||
|
||||
if (netChan.chan) {
|
||||
netChan.chan.favorite = false;
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.clearFavorites = function () {
|
||||
const client = this;
|
||||
|
||||
for (const favorite of client.favoriteChannels) {
|
||||
const netChan = client.find(favorite.channelId);
|
||||
|
||||
if (netChan.chan) {
|
||||
netChan.chan.favorite = false;
|
||||
}
|
||||
}
|
||||
|
||||
client.favoriteChannels = [];
|
||||
client.save();
|
||||
client.emitToAttachedClients("favorites", {
|
||||
favoriteChannels: client.favoriteChannels,
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.emitToAttachedClients = function (event, data) {
|
||||
const client = this;
|
||||
|
||||
for (const socketId in client.attachedClients) {
|
||||
const socket = client.manager.sockets.in(socketId);
|
||||
|
||||
if (socket) {
|
||||
socket.emit(event, data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ function Chan(attr) {
|
|||
highlight: 0,
|
||||
users: new Map(),
|
||||
muted: false,
|
||||
favorite: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -299,6 +300,18 @@ Chan.prototype.setMuteStatus = function (muted) {
|
|||
this.muted = !!muted;
|
||||
};
|
||||
|
||||
Chan.prototype.export = function () {
|
||||
const keys = ["name", "muted", "favorite"];
|
||||
|
||||
if (this.type === Chan.Type.CHANNEL) {
|
||||
keys.push("key");
|
||||
} else if (this.type === Chan.Type.QUERY) {
|
||||
keys.push("type");
|
||||
}
|
||||
|
||||
return _.pick(this, keys);
|
||||
};
|
||||
|
||||
function requestZncPlayback(channel, network, from) {
|
||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
||||
}
|
||||
|
|
|
@ -533,15 +533,7 @@ Network.prototype.export = function () {
|
|||
return channel.type === Chan.Type.CHANNEL || channel.type === Chan.Type.QUERY;
|
||||
})
|
||||
.map(function (chan) {
|
||||
const keys = ["name", "muted"];
|
||||
|
||||
if (chan.type === Chan.Type.CHANNEL) {
|
||||
keys.push("key");
|
||||
} else if (chan.type === Chan.Type.QUERY) {
|
||||
keys.push("type");
|
||||
}
|
||||
|
||||
return _.pick(chan, keys);
|
||||
return chan.export();
|
||||
});
|
||||
|
||||
return network;
|
||||
|
|
52
src/plugins/inputs/favorite.js
Normal file
52
src/plugins/inputs/favorite.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
"use strict";
|
||||
|
||||
const Msg = require("../../models/msg");
|
||||
const User = require("../../models/user");
|
||||
|
||||
exports.commands = ["favorite"];
|
||||
exports.allowDisconnected = true;
|
||||
|
||||
exports.input = function (network, chan, cmd, args) {
|
||||
const client = this;
|
||||
|
||||
if (args.length === 0) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Usage: /favorite [channel-or-conversation-name]`,
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (args.length === 1) {
|
||||
const channel = network.channels.find((c) => c.name === args[0]);
|
||||
|
||||
if (!channel) {
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
type: Msg.Type.ERROR,
|
||||
text: `Channel or conversation ${args[0]} not found.`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addToFavorites(network.uuid, channel.id);
|
||||
|
||||
chan.pushMessage(
|
||||
client,
|
||||
new Msg({
|
||||
// type: Msg.Type.ACTION,
|
||||
text: `Favorited ${channel.name}`,
|
||||
from: new User({
|
||||
nick: network.irc.user.nick,
|
||||
}),
|
||||
self: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.save();
|
||||
};
|
|
@ -36,6 +36,7 @@ const userInputs = [
|
|||
"topic",
|
||||
"whois",
|
||||
"mute",
|
||||
"favorite",
|
||||
].reduce(function (plugins, name) {
|
||||
const plugin = require(`./${name}`);
|
||||
plugin.commands.forEach((command) => plugins.set(command, plugin));
|
||||
|
|
|
@ -684,12 +684,10 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
}
|
||||
}
|
||||
|
||||
for (const attachedClient of Object.keys(client.attachedClients)) {
|
||||
manager.sockets.in(attachedClient).emit("mute:changed", {
|
||||
client.emitToAttachedClients("mute:changed", {
|
||||
target,
|
||||
status: setMutedTo,
|
||||
});
|
||||
}
|
||||
|
||||
client.save();
|
||||
});
|
||||
|
@ -726,6 +724,38 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("favorites:add", (channelId) => {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {network, chan} = client.find(channelId);
|
||||
|
||||
if (!network || !chan) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.addToFavorites(network.uuid, chan.id);
|
||||
});
|
||||
|
||||
socket.on("favorites:remove", (channelId) => {
|
||||
if (!channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {network, chan} = client.find(channelId);
|
||||
|
||||
if (!network || !chan) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.removeFromFavorites(chan.id);
|
||||
});
|
||||
|
||||
socket.on("favorites:clear", () => {
|
||||
client.clearFavorites();
|
||||
});
|
||||
|
||||
socket.join(client.id);
|
||||
|
||||
const sendInitEvent = (tokenToSend) => {
|
||||
|
@ -735,6 +765,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
|||
network.getFilteredClone(openChannel, lastMessage)
|
||||
),
|
||||
token: tokenToSend,
|
||||
favoriteChannels: client.favoriteChannels,
|
||||
});
|
||||
socket.emit("commands", inputs.getCommands());
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue