diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index 55ad9dd7..6f625f02 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -579,7 +579,8 @@ "cchange71": "and", "cchange72": "other", "cchange73": "s", - "cchange74": "reacted with" + "cchange74": "reacted with", + "cchange90": "No messages" }, "welcomepage": { "wcchange1": "Welcome to Q-Chat", diff --git a/qortal-ui-core/package.json b/qortal-ui-core/package.json index 5515942d..8e4f2ad3 100644 --- a/qortal-ui-core/package.json +++ b/qortal-ui-core/package.json @@ -77,9 +77,10 @@ "rollup-plugin-node-globals": "1.4.0", "rollup-plugin-progress": "1.1.2", "rollup-plugin-scss": "3.0.0", - "rollup-plugin-web-worker-loader": "1.6.1" + "rollup-plugin-web-worker-loader": "1.6.1", + "localforage": "1.10.0" }, "engines": { "node": ">=16.17.1" } -} \ No newline at end of file +} diff --git a/qortal-ui-core/src/components/app-view.js b/qortal-ui-core/src/components/app-view.js index cffc315f..60d02813 100644 --- a/qortal-ui-core/src/components/app-view.js +++ b/qortal-ui-core/src/components/app-view.js @@ -4,6 +4,11 @@ import { store } from '../store.js' import { Epml } from '../epml.js' import { addTradeBotRoutes } from '../tradebot/addTradeBotRoutes.js' import { get, translate, translateUnsafeHTML } from 'lit-translate' +import localForage from "localforage"; + +const chatLastSeen = localForage.createInstance({ + name: "chat-last-seen", +}); import '@polymer/paper-icon-button/paper-icon-button.js' import '@polymer/paper-progress/paper-progress.js' @@ -27,6 +32,7 @@ import './user-info-view/user-info-view.js' import '../functional-components/side-menu.js' import '../functional-components/side-menu-item.js' import './start-minting.js' +import { setChatLastSeen } from '../redux/app/app-actions.js' const parentEpml = new Epml({type: 'WINDOW', source: window.parent}) @@ -1386,6 +1392,17 @@ class AppView extends connect(store)(LitElement) { } } + const getChatLastSeen=async() => { + let items = []; + + await chatLastSeen.iterate(function(value, key, iterationNumber) { + + items.push({key, timestamp: value}); + }) + store.dispatch(setChatLastSeen(items)) + return items; + } + await getOpenTradesBTC() await appDelay(1000) await getOpenTradesLTC() @@ -1397,6 +1414,7 @@ class AppView extends connect(store)(LitElement) { await getOpenTradesRVN() await appDelay(1000) await getOpenTradesARRR() + await getChatLastSeen() } async getNodeType() { diff --git a/qortal-ui-core/src/components/login-view/login-view.js b/qortal-ui-core/src/components/login-view/login-view.js index aadc1973..ebdf31f2 100644 --- a/qortal-ui-core/src/components/login-view/login-view.js +++ b/qortal-ui-core/src/components/login-view/login-view.js @@ -14,12 +14,14 @@ import './login-section.js' import '../qort-theme-toggle.js' import settings from '../../functional-components/settings-page.js' -import { addAutoLoadImageChat, removeAutoLoadImageChat } from '../../redux/app/app-actions.js' +import { addAutoLoadImageChat, removeAutoLoadImageChat, addChatLastSeen } from '../../redux/app/app-actions.js' window.reduxStore = store window.reduxAction = { addAutoLoadImageChat: addAutoLoadImageChat, - removeAutoLoadImageChat: removeAutoLoadImageChat + removeAutoLoadImageChat: removeAutoLoadImageChat, + addChatLastSeen: addChatLastSeen + } const animationDuration = 0.7 // Seconds diff --git a/qortal-ui-core/src/plugins/streams.js b/qortal-ui-core/src/plugins/streams.js index ad6696d1..9cb6c3f7 100644 --- a/qortal-ui-core/src/plugins/streams.js +++ b/qortal-ui-core/src/plugins/streams.js @@ -9,6 +9,7 @@ const CHAT_HEADS_STREAM_NAME = 'chat_heads' const NODE_CONFIG_STREAM_NAME = 'node_config' const COPY_MENU_SWITCH = 'copy_menu_switch' const FRAME_PASTE_MENU_SWITCH = 'frame_paste_menu_switch' +const CHAT_LAST_SEEN = 'chat_last_seen' export const loggedInStream = new EpmlStream(LOGIN_STREAM_NAME, () => store.getState().app.loggedIn) export const configStream = new EpmlStream(CONFIG_STREAM_NAME, () => store.getState().config) @@ -18,6 +19,7 @@ export const chatHeadsStateStream = new EpmlStream(CHAT_HEADS_STREAM_NAME, () => export const nodeConfigStream = new EpmlStream(NODE_CONFIG_STREAM_NAME, () => store.getState().app.nodeConfig) export const copyMenuSwitchStream = new EpmlStream(COPY_MENU_SWITCH, () => store.getState().app.copyMenuSwitch) export const framePasteMenuSwitchStream = new EpmlStream(FRAME_PASTE_MENU_SWITCH, () => store.getState().app.framePasteMenuSwitch) +export const chatLastSeenStream = new EpmlStream(CHAT_LAST_SEEN, () => store.getState().app.chatLastSeen) let oldState = { @@ -46,6 +48,9 @@ store.subscribe(() => { if (oldState.app.framePasteMenuSwitch !== state.app.framePasteMenuSwitch) { framePasteMenuSwitchStream.emit(state.app.framePasteMenuSwitch) } + if (oldState.app.chatLastSeen !== state.app.chatLastSeen) { + chatLastSeenStream.emit(state.app.chatLastSeen) + } if (oldState.app.selectedAddress !== state.app.selectedAddress) { selectedAddressStream.emit({ diff --git a/qortal-ui-core/src/redux/app/actions/app-core.js b/qortal-ui-core/src/redux/app/actions/app-core.js index d91c2fa2..894f2c2e 100644 --- a/qortal-ui-core/src/redux/app/actions/app-core.js +++ b/qortal-ui-core/src/redux/app/actions/app-core.js @@ -1,5 +1,5 @@ // Core App Actions here... -import { UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, CHAT_HEADS, ACCOUNT_INFO, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT } from '../app-action-types.js' +import { UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, CHAT_HEADS, ACCOUNT_INFO, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN } from '../app-action-types.js' export const doUpdateBlockInfo = (blockObj) => { return (dispatch, getState) => { @@ -120,3 +120,16 @@ export const removeAutoLoadImageChat = (payload) => { } } +export const setChatLastSeen = (payload) => { + return { + type: SET_CHAT_LAST_SEEN, + payload + } +} +export const addChatLastSeen = (payload) => { + return { + type: ADD_CHAT_LAST_SEEN, + payload + } +} + diff --git a/qortal-ui-core/src/redux/app/app-action-types.js b/qortal-ui-core/src/redux/app/app-action-types.js index b0bb7813..518d4cd7 100644 --- a/qortal-ui-core/src/redux/app/app-action-types.js +++ b/qortal-ui-core/src/redux/app/app-action-types.js @@ -21,4 +21,6 @@ export const COPY_MENU_SWITCH = 'COPY_MENU_SWITCH' export const PASTE_MENU_SWITCH = 'PASTE_MENU_SWITCH' export const FRAME_PASTE_MENU_SWITCH = 'FRAME_PASTE_MENU_SWITCH' export const ADD_AUTO_LOAD_IMAGES_CHAT = 'ADD_AUTO_LOAD_IMAGES_CHAT' -export const REMOVE_AUTO_LOAD_IMAGES_CHAT = 'REMOVE_AUTO_LOAD_IMAGES_CHAT' \ No newline at end of file +export const REMOVE_AUTO_LOAD_IMAGES_CHAT = 'REMOVE_AUTO_LOAD_IMAGES_CHAT' +export const SET_CHAT_LAST_SEEN = 'SET_CHAT_LAST_SEEN' +export const ADD_CHAT_LAST_SEEN = 'ADD_CHAT_LAST_SEEN' diff --git a/qortal-ui-core/src/redux/app/app-reducer.js b/qortal-ui-core/src/redux/app/app-reducer.js index 6f3d8e1d..68a48675 100644 --- a/qortal-ui-core/src/redux/app/app-reducer.js +++ b/qortal-ui-core/src/redux/app/app-reducer.js @@ -1,9 +1,14 @@ // Loading state, login state, isNavDrawOpen state etc. None of this needs to be saved to localstorage. import { loadStateFromLocalStorage, saveStateToLocalStorage } from '../../localStorageHelpers.js' -import { LOG_IN, LOG_OUT, NETWORK_CONNECTION_STATUS, INIT_WORKERS, ADD_PLUGIN_URL, ADD_PLUGIN, ADD_NEW_PLUGIN_URL, NAVIGATE, SELECT_ADDRESS, ACCOUNT_INFO, CHAT_HEADS, UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, PAGE_URL, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT } from './app-action-types.js' +import { LOG_IN, LOG_OUT, NETWORK_CONNECTION_STATUS, INIT_WORKERS, ADD_PLUGIN_URL, ADD_PLUGIN, ADD_NEW_PLUGIN_URL, NAVIGATE, SELECT_ADDRESS, ACCOUNT_INFO, CHAT_HEADS, UPDATE_BLOCK_INFO, UPDATE_NODE_STATUS, UPDATE_NODE_INFO, LOAD_NODE_CONFIG, SET_NODE, ADD_NODE, PAGE_URL, COPY_MENU_SWITCH, PASTE_MENU_SWITCH, FRAME_PASTE_MENU_SWITCH, ADD_AUTO_LOAD_IMAGES_CHAT, REMOVE_AUTO_LOAD_IMAGES_CHAT, SET_CHAT_LAST_SEEN, ADD_CHAT_LAST_SEEN } from './app-action-types.js' import { initWorkersReducer } from './reducers/init-workers.js' import { loginReducer } from './reducers/login-reducer.js' import { setNode, addNode } from './reducers/manage-node.js' +import localForage from "localforage"; +const chatLastSeen = localForage.createInstance({ + name: "chat-last-seen", +}); + const INITIAL_STATE = { loggedIn: false, @@ -44,7 +49,8 @@ const INITIAL_STATE = { isOpen: false, elementId: '' }, - autoLoadImageChats: loadStateFromLocalStorage('autoLoadImageChats') || [] + autoLoadImageChats: loadStateFromLocalStorage('autoLoadImageChats') || [], + chatLastSeen: [] } export default (state = INITIAL_STATE, action) => { @@ -169,6 +175,38 @@ export default (state = INITIAL_STATE, action) => { autoLoadImageChats: updatedState } } + case SET_CHAT_LAST_SEEN: { + return { + ...state, + chatLastSeen: action.payload + } + } + case ADD_CHAT_LAST_SEEN: { + const chatId = action.payload.key + const timestamp = action.payload.timestamp + if(!chatId || !timestamp) return state + let newChatLastSeen = [...state.chatLastSeen] + const findChatIndex = state.chatLastSeen.findIndex((chat)=> chat.key === chatId) + if(findChatIndex !== -1){ + + newChatLastSeen[findChatIndex] = { + key: chatId, + timestamp, + } + } + if(findChatIndex === -1){ + + newChatLastSeen = [...newChatLastSeen, { + key: chatId, + timestamp, + }] + } + chatLastSeen.setItem(chatId, timestamp) + return { + ...state, + chatLastSeen: newChatLastSeen + } + } default: return state diff --git a/qortal-ui-plugins/plugins/core/components/ChatHead.js b/qortal-ui-plugins/plugins/core/components/ChatHead.js index c28a583f..7b706e0c 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatHead.js +++ b/qortal-ui-plugins/plugins/core/components/ChatHead.js @@ -1,11 +1,15 @@ import { LitElement, html, css } from 'lit' import { render } from 'lit/html.js' import { Epml } from '../../../epml.js' +import localForage from "localforage"; +import { translate} from 'lit-translate'; import '@material/mwc-icon' const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) - +const chatLastSeen = localForage.createInstance({ + name: "chat-last-seen", +}); class ChatHead extends LitElement { static get properties() { return { @@ -15,7 +19,8 @@ class ChatHead extends LitElement { iconName: { type: String }, activeChatHeadUrl: { type: String }, isImageLoaded: { type: Boolean }, - setActiveChatHeadUrl: {attribute: false} + setActiveChatHeadUrl: {attribute: false}, + lastReadMessageTimestamp: {type: Number} } } @@ -24,9 +29,13 @@ class ChatHead extends LitElement { li { width: 100%; - padding: 7px 5px 7px 5px; + padding: 10px 5px 10px 5px; cursor: pointer; width: 100%; + box-sizing: border-box; + display: flex; + align-items: flex-start; + } li:hover { @@ -44,12 +53,21 @@ class ChatHead extends LitElement { color: var(--chat-group); } - .about { - margin-top: 8px; - } + .about { - padding-left: 8px; + + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin: 0px; + } + .inner-container { + display: flex; + width: calc(100% - 45px); + flex-direction: column; + justify-content: center; } .status { @@ -64,6 +82,13 @@ class ChatHead extends LitElement { clear: both; height: 0; } + + .name { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } ` } @@ -82,6 +107,7 @@ class ChatHead extends LitElement { this.activeChatHeadUrl = '' this.isImageLoaded = false this.imageFetches = 0 + this.lastReadMessageTimestamp = 0 } createImage(imageUrl) { @@ -108,33 +134,59 @@ class ChatHead extends LitElement { }; return imageHTMLRes; } + updated(){ + } render() { let avatarImg = ''; let backupAvatarImg = '' + let isUnread = false + if(this.chatInfo.name){ const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]; const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port; const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`; avatarImg= this.createImage(avatarUrl) - } + if(this.lastReadMessageTimestamp && this.chatInfo.timestamp){ + if(this.lastReadMessageTimestamp < this.chatInfo.timestamp){ + isUnread = true + } + } + + if(this.activeChatHeadUrl === this.chatInfo.url){ + isUnread = false + } + return html`