Converse converse.js

Source: headless/plugins/roster/utils.js

  1. import log from "@converse/headless/log";
  2. import { Model } from '@converse/skeletor/src/model.js';
  3. import { RosterFilter } from '@converse/headless/plugins/roster/filter.js';
  4. import { STATUS_WEIGHTS } from "../../shared/constants";
  5. import { _converse, api, converse } from "@converse/headless/core";
  6. import { initStorage } from '@converse/headless/utils/storage.js';
  7. import { shouldClearCache } from '@converse/headless/utils/core.js';
  8. const { $pres } = converse.env;
  9. function initRoster () {
  10. // Initialize the collections that represent the roster contacts and groups
  11. const roster = _converse.roster = new _converse.RosterContacts();
  12. let id = `converse.contacts-${_converse.bare_jid}`;
  13. initStorage(roster, id);
  14. const filter = _converse.roster_filter = new RosterFilter();
  15. filter.id = `_converse.rosterfilter-${_converse.bare_jid}`;
  16. initStorage(filter, filter.id);
  17. filter.fetch();
  18. id = `converse-roster-model-${_converse.bare_jid}`;
  19. roster.data = new Model();
  20. roster.data.id = id;
  21. initStorage(roster.data, id);
  22. roster.data.fetch();
  23. /**
  24. * Triggered once the `_converse.RosterContacts`
  25. * been created, but not yet populated with data.
  26. * This event is useful when you want to create views for these collections.
  27. * @event _converse#chatBoxMaximized
  28. * @example _converse.api.listen.on('rosterInitialized', () => { ... });
  29. * @example _converse.api.waitUntil('rosterInitialized').then(() => { ... });
  30. */
  31. api.trigger('rosterInitialized');
  32. }
  33. /**
  34. * Fetch all the roster groups, and then the roster contacts.
  35. * Emit an event after fetching is done in each case.
  36. * @private
  37. * @param { Bool } ignore_cache - If set to to true, the local cache
  38. * will be ignored it's guaranteed that the XMPP server
  39. * will be queried for the roster.
  40. */
  41. async function populateRoster (ignore_cache=false) {
  42. if (ignore_cache) {
  43. _converse.send_initial_presence = true;
  44. }
  45. try {
  46. await _converse.roster.fetchRosterContacts();
  47. api.trigger('rosterContactsFetched');
  48. } catch (reason) {
  49. log.error(reason);
  50. } finally {
  51. _converse.send_initial_presence && api.user.presence.send();
  52. }
  53. }
  54. function updateUnreadCounter (chatbox) {
  55. const contact = _converse.roster?.get(chatbox.get('jid'));
  56. contact?.save({'num_unread': chatbox.get('num_unread')});
  57. }
  58. function registerPresenceHandler () {
  59. unregisterPresenceHandler();
  60. _converse.presence_ref = _converse.connection.addHandler(presence => {
  61. _converse.roster.presenceHandler(presence);
  62. return true;
  63. }, null, 'presence', null);
  64. }
  65. export function unregisterPresenceHandler () {
  66. if (_converse.presence_ref !== undefined) {
  67. _converse.connection.deleteHandler(_converse.presence_ref);
  68. delete _converse.presence_ref;
  69. }
  70. }
  71. async function clearPresences () {
  72. await _converse.presences?.clearStore();
  73. }
  74. /**
  75. * Roster specific event handler for the clearSession event
  76. */
  77. export async function onClearSession () {
  78. await clearPresences();
  79. if (shouldClearCache()) {
  80. if (_converse.rostergroups) {
  81. await _converse.rostergroups.clearStore();
  82. delete _converse.rostergroups;
  83. }
  84. if (_converse.roster) {
  85. _converse.roster.data?.destroy();
  86. await _converse.roster.clearStore();
  87. delete _converse.roster;
  88. }
  89. }
  90. }
  91. /**
  92. * Roster specific event handler for the presencesInitialized event
  93. * @param { Boolean } reconnecting
  94. */
  95. export function onPresencesInitialized (reconnecting) {
  96. if (reconnecting) {
  97. /**
  98. * Similar to `rosterInitialized`, but instead pertaining to reconnection.
  99. * This event indicates that the roster and its groups are now again
  100. * available after Converse.js has reconnected.
  101. * @event _converse#rosterReadyAfterReconnection
  102. * @example _converse.api.listen.on('rosterReadyAfterReconnection', () => { ... });
  103. */
  104. api.trigger('rosterReadyAfterReconnection');
  105. } else {
  106. initRoster();
  107. }
  108. _converse.roster.onConnected();
  109. registerPresenceHandler();
  110. populateRoster(!_converse.connection.restored);
  111. }
  112. /**
  113. * Roster specific event handler for the statusInitialized event
  114. * @param { Boolean } reconnecting
  115. */
  116. export async function onStatusInitialized (reconnecting) {
  117. if (reconnecting) {
  118. // When reconnecting and not resuming a previous session,
  119. // we clear all cached presence data, since it might be stale
  120. // and we'll receive new presence updates
  121. !_converse.connection.hasResumed() && (await clearPresences());
  122. } else {
  123. _converse.presences = new _converse.Presences();
  124. const id = `converse.presences-${_converse.bare_jid}`;
  125. initStorage(_converse.presences, id, 'session');
  126. // We might be continuing an existing session, so we fetch
  127. // cached presence data.
  128. _converse.presences.fetch();
  129. }
  130. /**
  131. * Triggered once the _converse.Presences collection has been
  132. * initialized and its cached data fetched.
  133. * Returns a boolean indicating whether this event has fired due to
  134. * Converse having reconnected.
  135. * @event _converse#presencesInitialized
  136. * @type { bool }
  137. * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
  138. */
  139. api.trigger('presencesInitialized', reconnecting);
  140. }
  141. /**
  142. * Roster specific event handler for the chatBoxesInitialized event
  143. */
  144. export function onChatBoxesInitialized () {
  145. _converse.chatboxes.on('change:num_unread', updateUnreadCounter);
  146. _converse.chatboxes.on('add', chatbox => {
  147. if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
  148. chatbox.setRosterContact(chatbox.get('jid'));
  149. }
  150. });
  151. }
  152. /**
  153. * Roster specific handler for the rosterContactsFetched promise
  154. */
  155. export function onRosterContactsFetched () {
  156. _converse.roster.on('add', contact => {
  157. // When a new contact is added, check if we already have a
  158. // chatbox open for it, and if so attach it to the chatbox.
  159. const chatbox = _converse.chatboxes.findWhere({ 'jid': contact.get('jid') });
  160. chatbox?.setRosterContact(contact.get('jid'));
  161. });
  162. }
  163. /**
  164. * Reject or cancel another user's subscription to our presence updates.
  165. * @function rejectPresenceSubscription
  166. * @param { String } jid - The Jabber ID of the user whose subscription is being canceled
  167. * @param { String } message - An optional message to the user
  168. */
  169. export function rejectPresenceSubscription (jid, message) {
  170. const pres = $pres({to: jid, type: "unsubscribed"});
  171. if (message && message !== "") { pres.c("status").t(message); }
  172. api.send(pres);
  173. }
  174. export function contactsComparator (contact1, contact2) {
  175. const status1 = contact1.presence.get('show') || 'offline';
  176. const status2 = contact2.presence.get('show') || 'offline';
  177. if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
  178. const name1 = (contact1.getDisplayName()).toLowerCase();
  179. const name2 = (contact2.getDisplayName()).toLowerCase();
  180. return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
  181. } else {
  182. return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
  183. }
  184. }
  185. export function groupsComparator (a, b) {
  186. const HEADER_WEIGHTS = {};
  187. HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
  188. HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
  189. HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS] = 2;
  190. HEADER_WEIGHTS[_converse.HEADER_UNGROUPED] = 3;
  191. HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS] = 4;
  192. const WEIGHTS = HEADER_WEIGHTS;
  193. const special_groups = Object.keys(HEADER_WEIGHTS);
  194. const a_is_special = special_groups.includes(a);
  195. const b_is_special = special_groups.includes(b);
  196. if (!a_is_special && !b_is_special ) {
  197. return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
  198. } else if (a_is_special && b_is_special) {
  199. return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
  200. } else if (!a_is_special && b_is_special) {
  201. const a_header = _converse.HEADER_CURRENT_CONTACTS;
  202. return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
  203. } else if (a_is_special && !b_is_special) {
  204. const b_header = _converse.HEADER_CURRENT_CONTACTS;
  205. return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
  206. }
  207. }
  208. export function getGroupsAutoCompleteList () {
  209. const { roster } = _converse;
  210. const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []);
  211. return [...new Set(groups.filter(i => i))];
  212. }