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>
|
<template>
|
||||||
<ChannelWrapper ref="wrapper" v-bind="$props">
|
<ChannelWrapper ref="wrapper" v-bind="$props">
|
||||||
<span class="name">{{ channel.name }}</span>
|
<span class="name">{{ name() }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="channel.unread"
|
v-if="channel.unread"
|
||||||
:class="{highlight: channel.highlight && !channel.muted}"
|
:class="{highlight: channel.highlight && !channel.muted}"
|
||||||
|
@ -51,6 +51,9 @@ export default {
|
||||||
close() {
|
close() {
|
||||||
this.$root.closeChannel(this.channel);
|
this.$root.closeChannel(this.channel);
|
||||||
},
|
},
|
||||||
|
name() {
|
||||||
|
return this.channel.displayName ? this.channel.displayName : this.channel.name;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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,
|
generateUserContextMenu,
|
||||||
generateChannelContextMenu,
|
generateChannelContextMenu,
|
||||||
generateInlineChannelContextMenu,
|
generateInlineChannelContextMenu,
|
||||||
|
generateFavoritesContextMenu,
|
||||||
} from "../js/helpers/contextMenu.js";
|
} from "../js/helpers/contextMenu.js";
|
||||||
import eventbus from "../js/eventbus";
|
import eventbus from "../js/eventbus";
|
||||||
|
|
||||||
|
@ -70,6 +71,7 @@ export default {
|
||||||
eventbus.on("contextmenu:user", this.openUserContextMenu);
|
eventbus.on("contextmenu:user", this.openUserContextMenu);
|
||||||
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
|
eventbus.on("contextmenu:channel", this.openChannelContextMenu);
|
||||||
eventbus.on("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
eventbus.on("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
||||||
|
eventbus.on("contextmenu:favorites", this.openFavoritesContextMenu);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
eventbus.off("escapekey", this.close);
|
eventbus.off("escapekey", this.close);
|
||||||
|
@ -77,6 +79,7 @@ export default {
|
||||||
eventbus.off("contextmenu:user", this.openUserContextMenu);
|
eventbus.off("contextmenu:user", this.openUserContextMenu);
|
||||||
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
|
eventbus.off("contextmenu:channel", this.openChannelContextMenu);
|
||||||
eventbus.off("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
eventbus.off("contextmenu:inline-channel", this.openInlineChannelContextMenu);
|
||||||
|
eventbus.off("contextmenu:favorites", this.openFavoritesContextMenu);
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
@ -119,6 +122,10 @@ export default {
|
||||||
);
|
);
|
||||||
this.open(data.event, items);
|
this.open(data.event, items);
|
||||||
},
|
},
|
||||||
|
openFavoritesContextMenu(data) {
|
||||||
|
const items = generateFavoritesContextMenu();
|
||||||
|
this.open(data.event, items);
|
||||||
|
},
|
||||||
open(event, items) {
|
open(event, items) {
|
||||||
event.preventDefault();
|
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"
|
@choose="onDraggableChoose"
|
||||||
@unchoose="onDraggableUnchoose"
|
@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
|
<div
|
||||||
v-for="network in $store.state.networks"
|
v-for="network in $store.state.networks"
|
||||||
:id="'network-' + network.uuid"
|
:id="'network-' + network.uuid"
|
||||||
|
@ -101,7 +114,6 @@
|
||||||
:channel="network.channels[0]"
|
:channel="network.channels[0]"
|
||||||
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
@toggle-join-channel="network.isJoinChannelShown = !network.isJoinChannelShown"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Draggable
|
<Draggable
|
||||||
draggable=".channel-list-item"
|
draggable=".channel-list-item"
|
||||||
ghost-class="ui-sortable-ghost"
|
ghost-class="ui-sortable-ghost"
|
||||||
|
@ -118,7 +130,7 @@
|
||||||
>
|
>
|
||||||
<template v-for="(channel, index) in network.channels">
|
<template v-for="(channel, index) in network.channels">
|
||||||
<Channel
|
<Channel
|
||||||
v-if="index > 0"
|
v-if="index > 0 && !channel.favorite"
|
||||||
:key="channel.id"
|
:key="channel.id"
|
||||||
:channel="channel"
|
:channel="channel"
|
||||||
:network="network"
|
:network="network"
|
||||||
|
@ -200,6 +212,7 @@ import Mousetrap from "mousetrap";
|
||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import {filter as fuzzyFilter} from "fuzzy";
|
import {filter as fuzzyFilter} from "fuzzy";
|
||||||
import NetworkLobby from "./NetworkLobby.vue";
|
import NetworkLobby from "./NetworkLobby.vue";
|
||||||
|
import Favorites from "./Favorites.vue";
|
||||||
import Channel from "./Channel.vue";
|
import Channel from "./Channel.vue";
|
||||||
import JoinChannel from "./JoinChannel.vue";
|
import JoinChannel from "./JoinChannel.vue";
|
||||||
|
|
||||||
|
@ -216,6 +229,7 @@ export default {
|
||||||
NetworkLobby,
|
NetworkLobby,
|
||||||
Channel,
|
Channel,
|
||||||
Draggable,
|
Draggable,
|
||||||
|
Favorites,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -263,6 +277,8 @@ export default {
|
||||||
Mousetrap.bind("alt+shift+right", this.expandNetwork);
|
Mousetrap.bind("alt+shift+right", this.expandNetwork);
|
||||||
Mousetrap.bind("alt+shift+left", this.collapseNetwork);
|
Mousetrap.bind("alt+shift+left", this.collapseNetwork);
|
||||||
Mousetrap.bind("alt+j", this.toggleSearch);
|
Mousetrap.bind("alt+j", this.toggleSearch);
|
||||||
|
|
||||||
|
console.log(this.$store.state.favoriteChannels[0]);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
Mousetrap.unbind("alt+shift+right", this.expandNetwork);
|
Mousetrap.unbind("alt+shift+right", this.expandNetwork);
|
||||||
|
|
|
@ -1,16 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<ChannelWrapper v-bind="$props" :channel="channel">
|
<ChannelWrapper v-bind="$props" :channel="channel">
|
||||||
<button
|
<CollapseNetworkButton :network="network" :on-collapse-click="onCollapseClick" />
|
||||||
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" />
|
|
||||||
<div class="lobby-wrap">
|
<div class="lobby-wrap">
|
||||||
<span :title="channel.name" class="name">{{ channel.name }}</span>
|
<span :title="channel.name" class="name">{{ channel.name }}</span>
|
||||||
<span
|
<span
|
||||||
|
@ -49,11 +39,13 @@
|
||||||
import collapseNetwork from "../js/helpers/collapseNetwork";
|
import collapseNetwork from "../js/helpers/collapseNetwork";
|
||||||
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
||||||
import ChannelWrapper from "./ChannelWrapper.vue";
|
import ChannelWrapper from "./ChannelWrapper.vue";
|
||||||
|
import CollapseNetworkButton from "./CollapseNetworkButton.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Channel",
|
name: "Channel",
|
||||||
components: {
|
components: {
|
||||||
ChannelWrapper,
|
ChannelWrapper,
|
||||||
|
CollapseNetworkButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
network: Object,
|
network: Object,
|
||||||
|
@ -76,9 +68,6 @@ export default {
|
||||||
onCollapseClick() {
|
onCollapseClick() {
|
||||||
collapseNetwork(this.network, !this.network.isCollapsed);
|
collapseNetwork(this.network, !this.network.isCollapsed);
|
||||||
},
|
},
|
||||||
getExpandLabel(network) {
|
|
||||||
return network.isCollapsed ? "Expand" : "Collapse";
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -369,6 +369,7 @@ p {
|
||||||
.context-menu-action-revoke-mode::before { content: "\f068"; /* http://fontawesome.io/icon/minus/ */ }
|
.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-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-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-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 */ }
|
.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);
|
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 {
|
#footer {
|
||||||
height: 45px;
|
height: 45px;
|
||||||
font-size: 14px;
|
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
|
// Add menu items for channels
|
||||||
|
@ -208,6 +223,21 @@ export function generateChannelContextMenu($root, channel, network) {
|
||||||
return items;
|
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) {
|
export function generateInlineChannelContextMenu($root, chan, network) {
|
||||||
const join = () => {
|
const join = () => {
|
||||||
const channel = network.channels.find((c) => c.name === chan);
|
const channel = network.channels.find((c) => c.name === chan);
|
||||||
|
|
|
@ -3,7 +3,12 @@
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
|
|
||||||
export default (network, channel) => {
|
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;
|
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 "./mentions";
|
||||||
import "./search";
|
import "./search";
|
||||||
import "./mute_changed";
|
import "./mute_changed";
|
||||||
|
import "./favorites";
|
||||||
|
|
|
@ -11,6 +11,7 @@ socket.on("init", function (data) {
|
||||||
store.commit("networks", mergeNetworkData(data.networks));
|
store.commit("networks", mergeNetworkData(data.networks));
|
||||||
store.commit("isConnected", true);
|
store.commit("isConnected", true);
|
||||||
store.commit("currentUserVisibleError", null);
|
store.commit("currentUserVisibleError", null);
|
||||||
|
store.commit("favoriteChannels", data.favoriteChannels);
|
||||||
|
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
storage.set("token", data.token);
|
storage.set("token", data.token);
|
||||||
|
|
|
@ -28,6 +28,7 @@ const store = new Vuex.Store({
|
||||||
isAutoCompleting: false,
|
isAutoCompleting: false,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
networks: [],
|
networks: [],
|
||||||
|
favoriteChannels: [],
|
||||||
mentions: [],
|
mentions: [],
|
||||||
hasServiceWorker: false,
|
hasServiceWorker: false,
|
||||||
pushNotificationState: "unsupported",
|
pushNotificationState: "unsupported",
|
||||||
|
@ -43,6 +44,7 @@ const store = new Vuex.Store({
|
||||||
messageSearchResults: null,
|
messageSearchResults: null,
|
||||||
messageSearchInProgress: false,
|
messageSearchInProgress: false,
|
||||||
searchEnabled: false,
|
searchEnabled: false,
|
||||||
|
favoritesOpen: storage.get("thelounge.state.favorites") !== "false",
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
appLoaded(state) {
|
appLoaded(state) {
|
||||||
|
@ -129,10 +131,59 @@ const store = new Vuex.Store({
|
||||||
|
|
||||||
state.messageSearchResults = value;
|
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: {
|
actions: {
|
||||||
partChannel({commit, state}, netChan) {
|
partChannel({commit, state}, netChan) {
|
||||||
const mentions = state.mentions.filter((msg) => !(msg.chanId === netChan.channel.id));
|
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);
|
commit("mentions", mentions);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -156,6 +207,21 @@ const store = new Vuex.Store({
|
||||||
|
|
||||||
return null;
|
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) => {
|
findChannel: (state) => (id) => {
|
||||||
for (const network of state.networks) {
|
for (const network of state.networks) {
|
||||||
for (const channel of network.channels) {
|
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(
|
store.watch(
|
||||||
(_, getters) => getters.title,
|
(_, getters) => getters.title,
|
||||||
(title) => {
|
(title) => {
|
||||||
|
|
|
@ -59,6 +59,7 @@ function Client(manager, name, config = {}) {
|
||||||
idMsg: 1,
|
idMsg: 1,
|
||||||
name: name,
|
name: name,
|
||||||
networks: [],
|
networks: [],
|
||||||
|
favoriteChannels: [],
|
||||||
mentions: [],
|
mentions: [],
|
||||||
manager: manager,
|
manager: manager,
|
||||||
messageStorage: [],
|
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
|
// Networks are stored directly in the client object
|
||||||
// We don't need to keep it in the config 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;
|
const client = this;
|
||||||
network.channels = _.without(network.channels, chan);
|
network.channels = _.without(network.channels, chan);
|
||||||
client.mentions = client.mentions.filter((msg) => !(msg.chanId === chan.id));
|
client.mentions = client.mentions.filter((msg) => !(msg.chanId === chan.id));
|
||||||
|
client.favoriteChannels = client.favoriteChannels.filter((fav) => fav.id !== chan.id);
|
||||||
chan.destroy();
|
chan.destroy();
|
||||||
client.save();
|
client.save();
|
||||||
client.emit("part", {
|
client.emit("part", {
|
||||||
|
@ -769,3 +782,81 @@ Client.prototype.save = _.debounce(
|
||||||
5000,
|
5000,
|
||||||
{maxWait: 20000}
|
{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,
|
highlight: 0,
|
||||||
users: new Map(),
|
users: new Map(),
|
||||||
muted: false,
|
muted: false,
|
||||||
|
favorite: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,6 +300,18 @@ Chan.prototype.setMuteStatus = function (muted) {
|
||||||
this.muted = !!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) {
|
function requestZncPlayback(channel, network, from) {
|
||||||
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
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;
|
return channel.type === Chan.Type.CHANNEL || channel.type === Chan.Type.QUERY;
|
||||||
})
|
})
|
||||||
.map(function (chan) {
|
.map(function (chan) {
|
||||||
const keys = ["name", "muted"];
|
return chan.export();
|
||||||
|
|
||||||
if (chan.type === Chan.Type.CHANNEL) {
|
|
||||||
keys.push("key");
|
|
||||||
} else if (chan.type === Chan.Type.QUERY) {
|
|
||||||
keys.push("type");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.pick(chan, keys);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return network;
|
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",
|
"topic",
|
||||||
"whois",
|
"whois",
|
||||||
"mute",
|
"mute",
|
||||||
|
"favorite",
|
||||||
].reduce(function (plugins, name) {
|
].reduce(function (plugins, name) {
|
||||||
const plugin = require(`./${name}`);
|
const plugin = require(`./${name}`);
|
||||||
plugin.commands.forEach((command) => plugins.set(command, plugin));
|
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)) {
|
client.emitToAttachedClients("mute:changed", {
|
||||||
manager.sockets.in(attachedClient).emit("mute:changed", {
|
target,
|
||||||
target,
|
status: setMutedTo,
|
||||||
status: setMutedTo,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
client.save();
|
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);
|
socket.join(client.id);
|
||||||
|
|
||||||
const sendInitEvent = (tokenToSend) => {
|
const sendInitEvent = (tokenToSend) => {
|
||||||
|
@ -735,6 +765,7 @@ function initializeClient(socket, client, token, lastMessage, openChannel) {
|
||||||
network.getFilteredClone(openChannel, lastMessage)
|
network.getFilteredClone(openChannel, lastMessage)
|
||||||
),
|
),
|
||||||
token: tokenToSend,
|
token: tokenToSend,
|
||||||
|
favoriteChannels: client.favoriteChannels,
|
||||||
});
|
});
|
||||||
socket.emit("commands", inputs.getCommands());
|
socket.emit("commands", inputs.getCommands());
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue