diff --git a/__tests__/lib/weechat/action_transformer.ts b/__tests__/lib/weechat/action_transformer.ts index 9dfc4c4..f2edb0f 100644 --- a/__tests__/lib/weechat/action_transformer.ts +++ b/__tests__/lib/weechat/action_transformer.ts @@ -123,6 +123,112 @@ describe('transformToReduxAction', () => { expect(store.getState().app.currentBufferId).toEqual('8578d9c00'); }); + + it('sets _id to the id provided by the relay', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: 'buffers', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [{ id: '1730555173010842', pointers: ['83a41cd80'] }] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().buffers).toHaveProperty('83a41cd80'); + const buffer = store.getState().buffers['83a41cd80']; + expect(buffer._id).toEqual(1730555173010842); + }); + + it('defaults _id to the buffer pointer', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: 'buffers', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [{ pointers: ['83a41cd80'] }] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().buffers).toHaveProperty('83a41cd80'); + const buffer = store.getState().buffers['83a41cd80']; + expect(buffer._id).toEqual(parseInt('83a41cd80', 16)); + }); + }); + + describe('on _buffer_opened', () => { + it('sets _id to the id provided by the relay', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: '_buffer_opened', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [{ id: '1730555173010842', pointers: ['83a41cd80'] }] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().buffers).toHaveProperty('83a41cd80'); + const buffer = store.getState().buffers['83a41cd80']; + expect(buffer._id).toEqual(1730555173010842); + }); + + it('defaults _id to the buffer pointer', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: '_buffer_opened', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [{ pointers: ['83a41cd80'] }] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().buffers).toHaveProperty('83a41cd80'); + const buffer = store.getState().buffers['83a41cd80']; + expect(buffer._id).toEqual(parseInt('83a41cd80', 16)); + }); }); describe('on _buffer_closing', () => { @@ -304,4 +410,177 @@ describe('transformToReduxAction', () => { }); }); }); + + describe('on lines', () => { + it('sets id to the id provided by the relay', () => { + const preloadedState = { + app: { + version: '4.4.0' + } as AppState + }; + const store = configureStore({ + reducer, + preloadedState, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: 'lines', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [ + { + id: 0, + buffer: '83a41cd80', + pointers: ['83a41cd80', '8493d36c0', '84d806c20', '85d064440'], + date: new Date('2024-11-09T00:02:07.000Z'), + date_printed: new Date('2024-11-10T17:28:48.000Z') + } + ] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().lines).toHaveProperty('83a41cd80'); + const lines = store.getState().lines['83a41cd80']; + expect(lines[0].id).toEqual(0); + }); + + it('defaults id to the line pointer', () => { + const preloadedState = { + app: { + version: '3.7.0' + } as AppState + }; + const store = configureStore({ + reducer, + preloadedState, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: 'lines', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [ + { + buffer: '83a41cd80', + pointers: ['83a41cd80', '8493d36c0', '84d806c20', '85d064440'], + date: new Date('2024-11-09T00:02:07.000Z'), + date_printed: new Date('2024-11-10T17:28:48.000Z') + } + ] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().lines).toHaveProperty('83a41cd80'); + const lines = store.getState().lines['83a41cd80']; + expect(lines[0].id).toEqual(parseInt('85d064440', 16)); + }); + }); + + describe('on _buffer_line_added', () => { + it('sets id to the id provided by the relay', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: '_buffer_line_added', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [ + { + id: 0, + buffer: '83a41cd80', + pointers: ['83a41cd80', '8493d36c0', '84d806c20', '85d064440'], + date: new Date('2024-11-09T00:02:07.000Z'), + date_printed: new Date('2024-11-10T17:28:48.000Z'), + tags_array: [] + } + ] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().lines).toHaveProperty('83a41cd80'); + const lines = store.getState().lines['83a41cd80']; + expect(lines[0].id).toEqual(0); + }); + + it('defaults id to the line pointer', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: '_buffer_line_added', + header: { compression: 0, length: 0 }, + objects: [ + { + type: 'hda', + content: [ + { + buffer: '83a41cd80', + pointers: ['83a41cd80', '8493d36c0', '84d806c20', '85d064440'], + date: new Date('2024-11-09T00:02:07.000Z'), + date_printed: new Date('2024-11-10T17:28:48.000Z'), + tags_array: [] + } + ] + } + ] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().lines).toHaveProperty('83a41cd80'); + const lines = store.getState().lines['83a41cd80']; + expect(lines[0].id).toEqual(parseInt('85d064440', 16)); + }); + }); + + describe('on version', () => { + it('stores the version', () => { + const store = configureStore({ + reducer, + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + + const action = transformToReduxAction({ + id: 'version', + header: { compression: 0, length: 0 }, + objects: [{ content: { key: 'version', value: '4.4.3' }, type: 'inf' }] + }); + expect(action).toBeDefined(); + + store.dispatch(action!); + + expect(store.getState().app.version).toEqual('4.4.3'); + }); + }); }); diff --git a/__tests__/store/listeners.ts b/__tests__/store/listeners.ts new file mode 100644 index 0000000..78b9d97 --- /dev/null +++ b/__tests__/store/listeners.ts @@ -0,0 +1,172 @@ +import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit'; +import { AppDispatch, reducer, StoreState } from '../../src/store'; +import { + fetchBuffersAction, + pendingBufferNotificationAction, + pongAction +} from '../../src/store/actions'; +import { PendingBufferNotificationListener } from '../../src/store/listeners'; +import { RelayClient } from '../../src/usecase/Root'; + +jest.useFakeTimers(); + +describe(PendingBufferNotificationListener, () => { + it('ensures that the relay is connected before dispatching the notification', async () => { + const client = new (class implements RelayClient { + isConnected = jest.fn(); + ping = jest.fn(); + })(); + + const listenerMiddleware = createListenerMiddleware(); + const store = configureStore({ + reducer, + preloadedState: { + buffers: { + ['83a41cd80']: { + _id: 1730555173010842, + full_name: 'irc.libera.#weechat', + hidden: 0, + id: '83a41cd80', + local_variables: { + channel: '#weechat', + name: 'libera.#weechat', + plugin: 'irc', + type: 'channel' + }, + notify: 3, + number: 2, + pointers: ['83a41cd80'], + short_name: '#weechat', + title: '', + type: 0 + } + } + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(listenerMiddleware.middleware), + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + listenerMiddleware.startListening.withTypes()( + PendingBufferNotificationListener(client) + ); + + client.isConnected.mockReturnValue(true); + + store.dispatch( + pendingBufferNotificationAction({ + identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9', + bufferId: 1730555173010842, + lineId: 0 + }) + ); + + store.dispatch(pongAction()); + await jest.runAllTimersAsync(); + + expect(client.isConnected).toHaveBeenCalled(); + expect(client.ping).toHaveBeenCalled(); + expect(store.getState().app.notification).not.toBeNull(); + expect(store.getState().app.notification!.identifier).toEqual( + '1fb4fc1d-530b-466f-85be-de27772de0a9' + ); + expect(store.getState().app.notification!.bufferId).toEqual('83a41cd80'); + expect(store.getState().app.notification!.lineId).toEqual(0); + }); + + it('refreshes the buffer list when disconnected before dispatching the notification', async () => { + const client = new (class implements RelayClient { + isConnected = jest.fn(); + ping = jest.fn(); + })(); + + const listenerMiddleware = createListenerMiddleware(); + const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(listenerMiddleware.middleware), + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + listenerMiddleware.startListening.withTypes()( + PendingBufferNotificationListener(client) + ); + + client.isConnected.mockReturnValue(false); + + store.dispatch( + pendingBufferNotificationAction({ + identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9', + bufferId: 1730555173010842, + lineId: 0 + }) + ); + + store.dispatch( + fetchBuffersAction({ + ['83a41cd80']: { + _id: 1730555173010842, + full_name: 'irc.libera.#weechat', + hidden: 0, + id: '83a41cd80', + local_variables: { + channel: '#weechat', + name: 'libera.#weechat', + plugin: 'irc', + type: 'channel' + }, + notify: 3, + number: 2, + pointers: ['83a41cd80'], + short_name: '#weechat', + title: '', + type: 0 + } + }) + ); + await jest.runAllTimersAsync(); + + expect(client.isConnected).toHaveBeenCalled(); + expect(store.getState().app.notification).not.toBeNull(); + expect(store.getState().app.notification!.identifier).toEqual( + '1fb4fc1d-530b-466f-85be-de27772de0a9' + ); + expect(store.getState().app.notification!.bufferId).toEqual('83a41cd80'); + expect(store.getState().app.notification!.lineId).toEqual(0); + }); + + it('does not dispatch the notification if the buffer is not in the buffer list', async () => { + const client = new (class implements RelayClient { + isConnected = jest.fn(); + ping = jest.fn(); + })(); + + const listenerMiddleware = createListenerMiddleware(); + const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(listenerMiddleware.middleware), + enhancers: (getDefaultEnhancers) => + getDefaultEnhancers({ autoBatch: false }) + }); + listenerMiddleware.startListening.withTypes()( + PendingBufferNotificationListener(client) + ); + + client.isConnected.mockReturnValue(false); + + store.dispatch( + pendingBufferNotificationAction({ + identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9', + bufferId: 1730555173010842, + lineId: 0 + }) + ); + + store.dispatch(fetchBuffersAction({})); + await jest.runAllTimersAsync(); + + expect(client.isConnected).toHaveBeenCalled(); + expect(store.getState().app.notification).toBeNull(); + }); +}); diff --git a/__tests__/usecase/App.tsx b/__tests__/usecase/App.tsx index 2813591..47e6959 100644 --- a/__tests__/usecase/App.tsx +++ b/__tests__/usecase/App.tsx @@ -18,6 +18,7 @@ describe('App', () => { preloadedState: { buffers: { [bufferId]: { + _id: 1730555173010842, full_name: 'irc.libera.#weechat', hidden: 0, id: bufferId, @@ -67,6 +68,7 @@ describe('App', () => { preloadedState: { buffers: { [bufferId]: { + _id: 1730555173010842, full_name: 'irc.libera.#weechat', hidden: 0, id: bufferId, @@ -104,7 +106,7 @@ describe('App', () => { store.dispatch( bufferNotificationAction({ bufferId, - lineId: '8580dcc40', + lineId: 0, identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9' }) ); diff --git a/__tests__/usecase/buffers/ui/Buffer.tsx b/__tests__/usecase/buffers/ui/Buffer.tsx index 5726223..e83c344 100644 --- a/__tests__/usecase/buffers/ui/Buffer.tsx +++ b/__tests__/usecase/buffers/ui/Buffer.tsx @@ -18,6 +18,7 @@ describe(Buffer, () => { const bufferRef = React.createRef(); const lines = [ { + id: 1, buffer: '86c417600', date: '2024-04-05T02:40:09.000Z', date_printed: '2024-04-06T17:20:30.000Z', @@ -29,6 +30,7 @@ describe(Buffer, () => { tags_array: ['irc_privmsg', 'notify_message'] } as WeechatLine, { + id: 0, buffer: '86c417600', date: '2024-04-05T02:40:09.000Z', date_printed: '2024-04-06T17:20:30.000Z', @@ -64,7 +66,7 @@ describe(Buffer, () => { bufferId={'86c417600'} fetchMoreLines={() => {}} clearNotification={() => {}} - notificationLineId="86c2fefd0" + notificationLineId={0} /> ); diff --git a/__tests__/usecase/buffers/ui/BufferContainer.tsx b/__tests__/usecase/buffers/ui/BufferContainer.tsx index 484e4d3..e9ac376 100644 --- a/__tests__/usecase/buffers/ui/BufferContainer.tsx +++ b/__tests__/usecase/buffers/ui/BufferContainer.tsx @@ -24,6 +24,7 @@ describe('BufferContainer', () => { preloadedState: { buffers: { [bufferId]: { + _id: 1730555173010842, full_name: 'irc.libera.#weechat', hidden: 0, id: bufferId, @@ -64,7 +65,7 @@ describe('BufferContainer', () => { store.dispatch( bufferNotificationAction({ bufferId, - lineId: '86c2ff040', + lineId: 0, identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9' }) ); @@ -82,6 +83,7 @@ describe('BufferContainer', () => { store.dispatch( fetchLinesAction([ { + id: 1730555173010842, buffer: '86c417600', date: '2024-04-05T02:40:09.000Z', date_printed: '2024-04-06T17:20:30.000Z', @@ -98,7 +100,7 @@ describe('BufferContainer', () => { expect(bufferContainer.props.notification).toEqual({ bufferId, - lineId: '86c2ff040', + lineId: 0, identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9' }); }); @@ -118,6 +120,7 @@ describe('BufferContainer', () => { preloadedState: { buffers: { [bufferId]: { + _id: 1730555173010842, full_name: 'irc.libera.#weechat', hidden: 0, id: bufferId, @@ -154,7 +157,7 @@ describe('BufferContainer', () => { store.dispatch( bufferNotificationAction({ bufferId, - lineId: '86c2ff040', + lineId: 0, identifier: '1fb4fc1d-530b-466f-85be-de27772de0a9' }) ); @@ -170,6 +173,7 @@ describe('BufferContainer', () => { store.dispatch( fetchLinesAction([ { + id: 0, buffer: '86c417600', date: '2024-04-05T02:40:09.000Z', date_printed: '2024-04-06T17:20:30.000Z', @@ -185,7 +189,7 @@ describe('BufferContainer', () => { }); expect(ActualBuffer.prototype.componentDidUpdate).toHaveBeenCalledWith( - expect.objectContaining({ notificationLineId: '86c2ff040' }), + expect.objectContaining({ notificationLineId: 0 }), expect.anything(), undefined ); diff --git a/scripts/weechat.pyi b/scripts/weechat.pyi index 8bb4ad8..a6bceec 100644 --- a/scripts/weechat.pyi +++ b/scripts/weechat.pyi @@ -1,5 +1,5 @@ # -# WeeChat Python stub file, auto-generated by python_stub.py. +# WeeChat Python stub file, auto-generated by generate_python_stub.py. # DO NOT EDIT BY HAND! # @@ -31,6 +31,9 @@ WEECHAT_HOTLIST_PRIVATE: str = "2" WEECHAT_HOTLIST_HIGHLIGHT: str = "3" WEECHAT_HOOK_PROCESS_RUNNING: int = -1 WEECHAT_HOOK_PROCESS_ERROR: int = -2 +WEECHAT_HOOK_CONNECT_IPV6_DISABLE: int = 0 +WEECHAT_HOOK_CONNECT_IPV6_AUTO: int = 1 +WEECHAT_HOOK_CONNECT_IPV6_FORCE: int = 2 WEECHAT_HOOK_CONNECT_OK: int = 0 WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND: int = 1 WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: int = 2 @@ -229,7 +232,7 @@ def string_remove_color(string: str, replacement: str) -> str: :: # example - str = weechat.string_remove_color(my_string, "?") + str = weechat.string_remove_color(my_string, "[color]") """ ... @@ -795,6 +798,17 @@ def config_boolean_default(option: str) -> int: ... +def config_boolean_inherited(option: str) -> int: + """`config_boolean_inherited in WeeChat plugin API reference `_ + :: + + # example + option = weechat.config_get("irc.server.libera.autoconnect") + autoconect = weechat.config_boolean_inherited(option) + """ + ... + + def config_integer(option: str) -> int: """`config_integer in WeeChat plugin API reference `_ :: @@ -817,6 +831,17 @@ def config_integer_default(option: str) -> int: ... +def config_integer_inherited(option: str) -> int: + """`config_integer_inherited in WeeChat plugin API reference `_ + :: + + # example + option = weechat.config_get("irc.server.libera.autojoin_delay") + delay = weechat.config_integer_inherited(option) + """ + ... + + def config_string(option: str) -> str: """`config_string in WeeChat plugin API reference `_ :: @@ -839,13 +864,24 @@ def config_string_default(option: str) -> str: ... +def config_string_inherited(option: str) -> str: + """`config_string_inherited in WeeChat plugin API reference `_ + :: + + # example + option = weechat.config_get("irc.server.libera.msg_quit") + msg_quit = weechat.config_string_inherited(option) + """ + ... + + def config_color(option: str) -> str: """`config_color in WeeChat plugin API reference `_ :: # example option = weechat.config_get("plugin.section.option") - value = weechat.config_color(option) + color = weechat.config_color(option) """ ... @@ -856,7 +892,18 @@ def config_color_default(option: str) -> str: # example option = weechat.config_get("plugin.section.option") - value = weechat.config_color_default(option) + color = weechat.config_color_default(option) + """ + ... + + +def config_color_inherited(option: str) -> str: + """`config_color_inherited in WeeChat plugin API reference `_ + :: + + # example + option = weechat.config_get("plugin.section.option") + color = weechat.config_color_inherited(option) """ ... @@ -883,6 +930,17 @@ def config_enum_default(option: str) -> int: ... +def config_enum_inherited(option: str) -> int: + """`config_enum_inherited in WeeChat plugin API reference `_ + :: + + # example + option = weechat.config_get("irc.server.libera.sasl_fail") + sasl_fail = weechat.config_enum_inherited(option) + """ + ... + + def config_write_option(config_file: str, option: str) -> int: """`config_write_option in WeeChat plugin API reference `_ :: @@ -1419,31 +1477,32 @@ def hook_connect(proxy: str, address: str, port: int, ipv6: int, retry: int, loc # example def my_connect_cb(data: str, status: int, gnutls_rc: int, sock: int, error: str, ip_address: str) -> int: - if status == WEECHAT_HOOK_CONNECT_OK: + if status == weechat.WEECHAT_HOOK_CONNECT_OK: # ... - elif status == WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND: + elif status == weechat.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND: # ... - elif status == WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: + elif status == weechat.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: # ... - elif status == WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED: + elif status == weechat.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED: # ... - elif status == WEECHAT_HOOK_CONNECT_PROXY_ERROR: + elif status == weechat.WEECHAT_HOOK_CONNECT_PROXY_ERROR: # ... - elif status == WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR: + elif status == weechat.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR: # ... - elif status == WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR: + elif status == weechat.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR: # ... - elif status == WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR: + elif status == weechat.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR: # ... - elif status == WEECHAT_HOOK_CONNECT_MEMORY_ERROR: + elif status == weechat.WEECHAT_HOOK_CONNECT_MEMORY_ERROR: # ... - elif status == WEECHAT_HOOK_CONNECT_TIMEOUT: + elif status == weechat.WEECHAT_HOOK_CONNECT_TIMEOUT: # ... - elif status == WEECHAT_HOOK_CONNECT_SOCKET_ERROR: + elif status == weechat.WEECHAT_HOOK_CONNECT_SOCKET_ERROR: # ... return weechat.WEECHAT_RC_OK - hook = weechat.hook_connect("", "my.server.org", 1234, 1, 0, "", + hook = weechat.hook_connect("", "my.server.org", 1234, + weechat.WEECHAT_HOOK_CONNECT_IPV6_AUTO, 0, "", "my_connect_cb", "") """ ... @@ -1871,6 +1930,16 @@ def buffer_match_list(buffer: str, string: str) -> int: ... +def line_search_by_id(buffer: str, line_id: int) -> str: + """`line_search_by_id in WeeChat plugin API reference `_ + :: + + # example + line = weechat.line_search_by_id(buffer, 123) + """ + ... + + def current_window() -> str: """`current_window in WeeChat plugin API reference `_ :: @@ -1946,8 +2015,9 @@ def nicklist_search_group(buffer: str, from_group: str, name: str) -> str: """`nicklist_search_group in WeeChat plugin API reference `_ :: - # example - group = weechat.nicklist_search_group(my_buffer, "", "test_group") + # examples + group1 = weechat.nicklist_search_group(my_buffer, "", "test_group") + group2 = weechat.nicklist_search_group(my_buffer, "", "==id:1714382231198764") """ ... @@ -1970,8 +2040,9 @@ def nicklist_search_nick(buffer: str, from_group: str, name: str) -> str: """`nicklist_search_nick in WeeChat plugin API reference `_ :: - # example - nick = weechat.nicklist_search_nick(my_buffer, "", "test_nick") + # examples + nick1 = weechat.nicklist_search_nick(my_buffer, "", "test_nick") + nick2 = weechat.nicklist_search_nick(my_buffer, "", "==id:1714382252187496") """ ... @@ -2223,7 +2294,7 @@ def command_options(buffer: str, command: str, options: Dict[str, str]) -> int: :: # example: allow any command except /exec - rc = weechat.command("", "/some_command arguments", {"commands": "*,!exec"}) + rc = weechat.command_options("", "/some_command arguments", {"commands": "*,!exec"}) """ ... @@ -2475,7 +2546,7 @@ def infolist_time(infolist: str, var: str) -> int: :: # example - weechat.prnt("", "time = %ld" % weechat.infolist_time(infolist, "my_time")) + weechat.prnt("", "time = %d" % weechat.infolist_time(infolist, "my_time")) """ ... @@ -2641,7 +2712,17 @@ def hdata_long(hdata: str, pointer: str, name: str) -> int: :: # example - weechat.prnt("", "longvar = %ld" % weechat.hdata_long(hdata, pointer, "longvar")) + weechat.prnt("", "longvar = %d" % weechat.hdata_long(hdata, pointer, "longvar")) + """ + ... + + +def hdata_longlong(hdata: str, pointer: str, name: str) -> int: + """`hdata_longlong in WeeChat plugin API reference `_ + :: + + # example + weechat.prnt("", "longlongvar = %d" % weechat.hdata_longlong(hdata, pointer, "longlongvar")) """ ... @@ -2665,7 +2746,7 @@ def hdata_pointer(hdata: str, pointer: str, name: str) -> str: # example hdata = weechat.hdata_get("buffer") buffer = weechat.buffer_search_main() - weechat.prnt("", "lines = %lx" % weechat.hdata_pointer(hdata, buffer, "lines")) + weechat.prnt("", "lines = %x" % weechat.hdata_pointer(hdata, buffer, "lines")) """ ... diff --git a/scripts/weechatrn.py b/scripts/weechatrn.py index 73c54b5..093a0cf 100644 --- a/scripts/weechatrn.py +++ b/scripts/weechatrn.py @@ -79,12 +79,24 @@ def priv_msg_cb( if not script_options.notify_current_buffer and weechat.current_buffer() == buffer: return weechat.WEECHAT_RC_OK - line_data = "" + version = weechat.info_get("version_number", "") or 0 + buffer_id = ( + weechat.hdata_longlong(weechat.hdata_get("buffer"), buffer, "id") + if int(version) >= 0x04030000 + else buffer + ) + line = line_data = line_id = None own_lines = weechat.hdata_pointer(weechat.hdata_get("buffer"), buffer, "own_lines") if own_lines: line = weechat.hdata_pointer(weechat.hdata_get("lines"), own_lines, "last_line") - if line: - line_data = weechat.hdata_pointer(weechat.hdata_get("line"), line, "data") + if line: + line_data = weechat.hdata_pointer(weechat.hdata_get("line"), line, "data") + if line_data: + line_id = ( + weechat.hdata_integer(weechat.hdata_get("line_data"), line_data, "id") + if int(version) >= 0x04040000 + else line_data + ) body = f"<{prefix}> {message}" is_pm = weechat.buffer_get_string(buffer, "localvar_type") == "private" @@ -92,7 +104,7 @@ def priv_msg_cb( send_push( f"Private message from {prefix}", body, - {"bufferId": buffer, "lineId": line_data}, + {"bufferId": buffer_id, "lineId": line_id}, ) elif int(highlight): buffer_name = weechat.buffer_get_string( @@ -101,13 +113,13 @@ def priv_msg_cb( send_push( f"Highlight in {buffer_name}", body, - {"bufferId": buffer, "lineId": line_data}, + {"bufferId": buffer_id, "lineId": line_id}, ) return weechat.WEECHAT_RC_OK -def send_push(title: str, body: str, data: dict[str, str]) -> None: +def send_push(title: str, body: str, data: dict[str, object]) -> None: """ Send push notification to Expo server. Message JSON encoded in the format: [{ "to": "EXPO_PUSH_TOKEN", @@ -173,7 +185,7 @@ def main(): if weechat.register( "WeechatRN", "mhoran", - "1.2.0", + "1.3.0", "MIT", "WeechatRN push notification plugin", "", diff --git a/src/lib/weechat/action_transformer.ts b/src/lib/weechat/action_transformer.ts index 24c35e4..10bd22b 100644 --- a/src/lib/weechat/action_transformer.ts +++ b/src/lib/weechat/action_transformer.ts @@ -31,6 +31,11 @@ const reduceToObjectByKey = ( mapFn: MapFn = (a) => a ) => array.reduce((acc, elem) => ({ ...acc, [keyFn(elem)]: mapFn(elem) }), {}); +const parseVersion = (version: string) => { + const parts = version.split('.').map((part) => parseInt(part) || 0); + return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; +}; + export const transformToReduxAction = ( data: WeechatResponse ): @@ -91,13 +96,18 @@ export const transformToReduxAction = ( getState: () => StoreState ) => { const state: StoreState = getState(); - const { date, date_printed, ...restLine } = line; + const { id, date, date_printed, ...restLine } = line; + const pointers = restLine.pointers as string[]; dispatch( bufferLineAddedAction({ currentBufferId: state.app.currentBufferId, line: { ...restLine, + id: + id !== undefined + ? id + : parseInt(pointers[pointers.length - 1], 16), date: (date as Date).toISOString(), date_printed: (date_printed as Date).toISOString() } as WeechatLine @@ -114,6 +124,10 @@ export const transformToReduxAction = ( case '_buffer_opened': { const object = data.objects[0] as WeechatObject; const buffer = object.content[0]; + buffer._id = + buffer.id !== undefined + ? parseInt(buffer.id) + : parseInt(buffer.pointers[0], 16); buffer.id = buffer.pointers[0]; return bufferOpenedAction(buffer); @@ -194,7 +208,14 @@ export const transformToReduxAction = ( const newBuffers = reduceToObjectByKey( object.content, (buffer) => buffer.pointers[0], - (buf) => ({ ...buf, id: buf.pointers[0] }) + (buf) => ({ + ...buf, + id: buf.pointers[0], + _id: + buf.id !== undefined + ? parseInt(buf.id) + : parseInt(buf.pointers[0], 16) + }) ); const removed = Object.keys(buffers).filter((buffer) => { return !(buffer in newBuffers); @@ -214,17 +235,29 @@ export const transformToReduxAction = ( Record[] >; if (!object.content[0]) return undefined; - return fetchLinesAction( - object.content.map((line) => { - const { date, date_printed, ...restLine } = line; - - return { - ...restLine, - date: (date as Date).toISOString(), - date_printed: (date_printed as Date).toISOString() - } as WeechatLine; - }) - ); + return ( + dispatch: ThunkDispatch, + getState: () => StoreState + ) => { + dispatch( + fetchLinesAction( + object.content.map((line) => { + const { id, date, date_printed, ...restLine } = line; + const pointers = restLine.pointers as string[]; + + return { + ...restLine, + id: + parseVersion(getState().app.version) >= 0x04040000 + ? id + : parseInt(pointers[pointers.length - 1], 16), + date: (date as Date).toISOString(), + date_printed: (date_printed as Date).toISOString() + } as WeechatLine; + }) + ) + ); + }; } case 'last_read_lines': { const object = data.objects[0] as WeechatObject; diff --git a/src/lib/weechat/types.d.ts b/src/lib/weechat/types.d.ts index 000e675..b65538b 100644 --- a/src/lib/weechat/types.d.ts +++ b/src/lib/weechat/types.d.ts @@ -18,6 +18,7 @@ interface WeechatInfoList { interface WeechatBuffer { id: string; + _id: number; pointers: string[]; local_variables: Localvariables; notify: number; @@ -77,6 +78,7 @@ interface Header { } interface WeechatLine { + id: number; pointers: string[]; prefix_length: number; prefix: string; @@ -90,4 +92,5 @@ interface WeechatLine { tags_array: string[]; buffer: string; highlight: number; + y: number; } diff --git a/src/store/actions.ts b/src/store/actions.ts index 0c51343..20cb651 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -19,9 +19,14 @@ export const changeCurrentBufferAction = createAction( 'CHANGE_CURRENT_BUFFER' ); +export const pendingBufferNotificationAction = createAction<{ + identifier: string; + bufferId: number; + lineId: number; +}>('PENDING_BUFFER_NOTIFICATION'); export const bufferNotificationAction = createAction<{ bufferId: string; - lineId: string; + lineId: number; identifier: string; }>('BUFFER_NOTIFICATION'); export const clearBufferNotificationAction = createAction( diff --git a/src/store/app.ts b/src/store/app.ts index a60f6a1..c0dec3a 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -14,15 +14,17 @@ import { export type AppState = { connected: boolean; currentBufferId: string | null; - notification: { bufferId: string; lineId: string; identifier: string } | null; + notification: { bufferId: string; lineId: number; identifier: string } | null; currentBufferLinesFetched: boolean; + version: string; }; const initialState: AppState = { connected: false, currentBufferId: null, notification: null, - currentBufferLinesFetched: false + currentBufferLinesFetched: false, + version: '' }; export const app = createReducer(initialState, (builder) => { @@ -33,10 +35,11 @@ export const app = createReducer(initialState, (builder) => { currentBufferLinesFetched: false }; }); - builder.addCase(fetchVersionAction, (state) => { + builder.addCase(fetchVersionAction, (state, action) => { return { ...state, - connected: true + connected: true, + version: action.payload }; }); builder.addCase(changeCurrentBufferAction, (state, action) => { diff --git a/src/store/listeners.ts b/src/store/listeners.ts new file mode 100644 index 0000000..8514d2b --- /dev/null +++ b/src/store/listeners.ts @@ -0,0 +1,45 @@ +import { ListenerEffectAPI, isAnyOf } from '@reduxjs/toolkit'; +import { AppDispatch, StoreState } from '.'; +import { RelayClient } from '../usecase/Root'; +import { + bufferNotificationAction, + disconnectAction, + fetchBuffersAction, + pendingBufferNotificationAction, + pongAction +} from './actions'; + +export const PendingBufferNotificationListener = (client: RelayClient) => ({ + actionCreator: pendingBufferNotificationAction, + effect: async ( + action: ReturnType, + listenerApi: ListenerEffectAPI + ) => { + listenerApi.cancelActiveListeners(); + + let reallyConnected = false; + + if (client.isConnected()) { + client.ping(); + const [responseAction] = await listenerApi.take( + isAnyOf(pongAction, disconnectAction) + ); + reallyConnected = pongAction.match(responseAction); + } + if (!reallyConnected) { + await listenerApi.condition(fetchBuffersAction.match); + } + + const buffer = Object.values(listenerApi.getState().buffers).find( + (buffer) => buffer._id === action.payload.bufferId + ); + if (!buffer) return; + + listenerApi.dispatch( + bufferNotificationAction({ + ...action.payload, + bufferId: buffer.id + }) + ); + } +}); diff --git a/src/usecase/Root.tsx b/src/usecase/Root.tsx index 15056f8..ab372f3 100644 --- a/src/usecase/Root.tsx +++ b/src/usecase/Root.tsx @@ -5,39 +5,44 @@ import { Provider } from 'react-redux'; import WeechatConnection, { ConnectionError } from '../lib/weechat/connection'; import { AppDispatch, StoreState, store } from '../store'; -import { - UnsubscribeListener, - addListener, - createAction, - isAnyOf -} from '@reduxjs/toolkit'; +import { UnsubscribeListener, addListener } from '@reduxjs/toolkit'; import * as Notifications from 'expo-notifications'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { getPushNotificationStatusAsync } from '../lib/helpers/push-notifications'; import { - bufferNotificationAction, - disconnectAction, - fetchBuffersAction, fetchScriptsAction, - pongAction, + pendingBufferNotificationAction, upgradeAction } from '../store/actions'; import App from './App'; -import ConnectionGate from './ConnectionGate'; import Buffer from './buffers/ui/Buffer'; +import ConnectionGate from './ConnectionGate'; import PersistGate from './PersistGate'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; - -const fetchBuffersDispatchAction = createAction< - ReturnType ->('FETCH_BUFFERS_DISPATCH'); +import { PendingBufferNotificationListener } from '../store/listeners'; interface State { connecting: boolean; connectionError: ConnectionError | null; } +export interface RelayClient { + isConnected: () => boolean; + ping: () => void; +} + export default class WeechatNative extends React.Component { + static RelayClient = class implements RelayClient { + parent: WeechatNative; + + constructor(parent: WeechatNative) { + this.parent = parent; + } + + isConnected = () => this.parent.connection?.isConnected() || false; + ping = () => this.parent.connection?.send('ping'); + }; + state: State = { connecting: false, connectionError: null @@ -47,6 +52,8 @@ export default class WeechatNative extends React.Component { connection?: WeechatConnection; + client = new WeechatNative.RelayClient(this); + appStateListener = AppState.addEventListener('change', (nextAppState) => { if (nextAppState === 'active') { this.onResume(); @@ -61,20 +68,18 @@ export default class WeechatNative extends React.Component { if (!bufferId || !lineId) return; store.dispatch( - fetchBuffersDispatchAction( - bufferNotificationAction({ - identifier: request.identifier, - bufferId: bufferId.replace(/^0x/, ''), - lineId: lineId.replace(/^0x/, '') - }) - ) + pendingBufferNotificationAction({ + identifier: request.identifier, + bufferId: Number(bufferId), + lineId: Number(lineId) + }) ); } ); unsubscribeUpgradeListener: UnsubscribeListener; unsubscribeFetchScriptsListener: UnsubscribeListener; - unsubscribeFetchBuffersDispatchListener: UnsubscribeListener; + unsubscribePendingBufferNotificationListener: UnsubscribeListener; constructor(props: null) { super(props); @@ -95,32 +100,10 @@ export default class WeechatNative extends React.Component { } }) ); - this.unsubscribeFetchBuffersDispatchListener = store.dispatch( - addListener.withTypes()({ - actionCreator: fetchBuffersDispatchAction, - effect: async (action, listenerApi) => { - listenerApi.cancelActiveListeners(); - - let reallyConnected = false; - - if (this.connection?.isConnected()) { - this.connection.send('ping'); - const [responseAction] = await listenerApi.take( - isAnyOf(pongAction, disconnectAction) - ); - reallyConnected = pongAction.match(responseAction); - } - - if (!reallyConnected) { - await listenerApi.condition(fetchBuffersAction.match); - } - - const wrappedAction = action.payload; - if (listenerApi.getState().buffers[wrappedAction.payload.bufferId]) { - listenerApi.dispatch(wrappedAction); - } - } - }) + this.unsubscribePendingBufferNotificationListener = store.dispatch( + addListener.withTypes()( + PendingBufferNotificationListener(this.client) + ) ); } @@ -130,7 +113,7 @@ export default class WeechatNative extends React.Component { this.unsubscribeUpgradeListener(); this.unsubscribeFetchScriptsListener(); this.responseListener.remove(); - this.unsubscribeFetchBuffersDispatchListener(); + this.unsubscribePendingBufferNotificationListener(); } onBeforeLift = (): void => { @@ -148,7 +131,7 @@ export default class WeechatNative extends React.Component { this.setState({ connecting: false }); connection.send('(hotlist) hdata hotlist:gui_hotlist(*)'); connection.send( - '(buffers) hdata buffer:gui_buffers(*) local_variables,notify,number,full_name,short_name,title,hidden,type' + '(buffers) hdata buffer:gui_buffers(*) id,local_variables,notify,number,full_name,short_name,title,hidden,type' ); connection.send('(scripts) hdata python_script:scripts(*) name'); connection.send('sync'); diff --git a/src/usecase/buffers/ui/Buffer.tsx b/src/usecase/buffers/ui/Buffer.tsx index b1fa1e6..2945631 100644 --- a/src/usecase/buffers/ui/Buffer.tsx +++ b/src/usecase/buffers/ui/Buffer.tsx @@ -21,7 +21,7 @@ interface Props { parseArgs: ParseShape[]; bufferId: string; fetchMoreLines: (lines: number) => void; - notificationLineId?: string; + notificationLineId?: number; clearNotification: () => void; } @@ -74,20 +74,18 @@ export default class Buffer extends React.PureComponent { initialNumToRender: 35 }; - notificationLineId: string | null = null; + notificationLineId: number | null = null; componentDidUpdate(prevProps: Readonly): void { const { notificationLineId, clearNotification, lines } = this.props; if ( - notificationLineId && + notificationLineId !== undefined && notificationLineId !== prevProps.notificationLineId ) { clearNotification(); - const index = lines.findIndex( - (line) => line.pointers.at(-1) === notificationLineId - ); + const index = lines.findIndex((line) => line.id === notificationLineId); if (index < 0) return; this.notificationLineId = notificationLineId; @@ -138,8 +136,8 @@ export default class Buffer extends React.PureComponent { ...props }) => { const isNotificationLine = - this.notificationLineId && - this.notificationLineId === item.pointers.at(-1); + this.notificationLineId !== undefined && + item.id === this.notificationLineId; const onLayout = (event: LayoutChangeEvent) => { props.onLayout?.(event);