utils.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /**
  2. * @typedef {import('@converse/skeletor').Model} Model
  3. * @typedef {import('@converse/headless').RosterContact} RosterContact
  4. * @typedef {import('@converse/headless').RosterContacts} RosterContacts
  5. */
  6. import { __ } from 'i18n';
  7. import { _converse, api, converse, log, constants } from "@converse/headless";
  8. const { Strophe } = converse.env;
  9. const { STATUS_WEIGHTS } = constants;
  10. export function removeContact (contact) {
  11. contact.removeFromRoster(
  12. () => contact.destroy(),
  13. (e) => {
  14. e && log.error(e);
  15. api.alert('error', __('Error'), [
  16. __('Sorry, there was an error while trying to remove %1$s as a contact.',
  17. contact.getDisplayName())
  18. ]);
  19. }
  20. );
  21. }
  22. export function highlightRosterItem (chatbox) {
  23. _converse.state.roster?.get(chatbox.get('jid'))?.trigger('highlight');
  24. }
  25. export function toggleGroup (ev, name) {
  26. ev?.preventDefault?.();
  27. const { roster } = _converse.state;
  28. const collapsed = roster.state.get('collapsed_groups');
  29. if (collapsed.includes(name)) {
  30. roster.state.save('collapsed_groups', collapsed.filter(n => n !== name));
  31. } else {
  32. roster.state.save('collapsed_groups', [...collapsed, name]);
  33. }
  34. }
  35. /**
  36. * @param {RosterContact} contact
  37. * @param {string} groupname
  38. * @returns {boolean}
  39. */
  40. export function isContactFiltered (contact, groupname) {
  41. const filter = _converse.state.roster_filter;
  42. const type = filter.get('type');
  43. const q = (type === 'state') ?
  44. filter.get('state').toLowerCase() :
  45. filter.get('text').toLowerCase();
  46. if (!q) return false;
  47. if (type === 'state') {
  48. const sticky_groups = [_converse.labels.HEADER_REQUESTING_CONTACTS, _converse.labels.HEADER_UNREAD];
  49. if (sticky_groups.includes(groupname)) {
  50. // When filtering by chat state, we still want to
  51. // show sticky groups, even though they don't
  52. // match the state in question.
  53. return false;
  54. } else if (q === 'unread_messages') {
  55. return contact.get('num_unread') === 0;
  56. } else if (q === 'online') {
  57. return ["offline", "unavailable", "dnd", "away", "xa"].includes(contact.presence.get('show'));
  58. } else {
  59. return !contact.presence.get('show').includes(q);
  60. }
  61. } else if (type === 'items') {
  62. return !contact.getFilterCriteria().includes(q);
  63. }
  64. }
  65. /**
  66. * @param {RosterContact} contact
  67. * @param {string} groupname
  68. * @param {Model} model
  69. * @returns {boolean}
  70. */
  71. export function shouldShowContact (contact, groupname, model) {
  72. if (!model.get('filter_visible')) return true;
  73. const chat_status = contact.presence.get('show');
  74. if (api.settings.get('hide_offline_users') && chat_status === 'offline') {
  75. // If pending or requesting, show
  76. if ((contact.get('ask') === 'subscribe') ||
  77. (contact.get('subscription') === 'from') ||
  78. (contact.get('requesting') === true)) {
  79. return !isContactFiltered(contact, groupname);
  80. }
  81. return false;
  82. }
  83. return !isContactFiltered(contact, groupname);
  84. }
  85. export function shouldShowGroup (group, model) {
  86. if (!model.get('filter_visible')) return true;
  87. const filter = _converse.state.roster_filter;
  88. const type = filter.get('type');
  89. if (type === 'groups') {
  90. const q = filter.get('text')?.toLowerCase();
  91. if (!q) {
  92. return true;
  93. }
  94. if (!group.toLowerCase().includes(q)) {
  95. return false;
  96. }
  97. }
  98. return true;
  99. }
  100. /**
  101. * @param {import('./types').ContactsMap} contacts_map
  102. * @param {RosterContact} contact
  103. * @returns {import('./types').ContactsMap}
  104. */
  105. export function populateContactsMap (contacts_map, contact) {
  106. const { labels } = _converse;
  107. let contact_groups;
  108. if (contact.get('requesting')) {
  109. contact_groups = [labels.HEADER_REQUESTING_CONTACTS];
  110. } else if (contact.get('ask') === 'subscribe') {
  111. contact_groups = [labels.HEADER_PENDING_CONTACTS];
  112. } else if (contact.get('subscription') === 'none') {
  113. contact_groups = [labels.HEADER_UNSAVED_CONTACTS];
  114. } else if (!api.settings.get('roster_groups')) {
  115. contact_groups = [labels.HEADER_CURRENT_CONTACTS];
  116. } else {
  117. contact_groups = contact.get('groups');
  118. contact_groups = (contact_groups.length === 0) ? [labels.HEADER_UNGROUPED] : contact_groups;
  119. }
  120. for (const name of contact_groups) {
  121. contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
  122. }
  123. if (contact.get('num_unread')) {
  124. const name = /** @type {string} */(labels.HEADER_UNREAD);
  125. contacts_map[name] ? contacts_map[name].push(contact) : (contacts_map[name] = [contact]);
  126. }
  127. return contacts_map;
  128. }
  129. export function contactsComparator (contact1, contact2) {
  130. const status1 = contact1.presence.get('show') || 'offline';
  131. const status2 = contact2.presence.get('show') || 'offline';
  132. if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
  133. const name1 = (contact1.getDisplayName()).toLowerCase();
  134. const name2 = (contact2.getDisplayName()).toLowerCase();
  135. return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
  136. } else {
  137. return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
  138. }
  139. }
  140. export function groupsComparator (a, b) {
  141. const HEADER_WEIGHTS = {};
  142. const {
  143. HEADER_UNREAD,
  144. HEADER_REQUESTING_CONTACTS,
  145. HEADER_CURRENT_CONTACTS,
  146. HEADER_UNGROUPED,
  147. HEADER_PENDING_CONTACTS,
  148. } = _converse.labels;
  149. HEADER_WEIGHTS[HEADER_UNREAD] = 0;
  150. HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 1;
  151. HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 2;
  152. HEADER_WEIGHTS[HEADER_UNGROUPED] = 3;
  153. HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 4;
  154. const WEIGHTS = HEADER_WEIGHTS;
  155. const special_groups = Object.keys(HEADER_WEIGHTS);
  156. const a_is_special = special_groups.includes(a);
  157. const b_is_special = special_groups.includes(b);
  158. if (!a_is_special && !b_is_special ) {
  159. return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
  160. } else if (a_is_special && b_is_special) {
  161. return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
  162. } else if (!a_is_special && b_is_special) {
  163. const a_header = HEADER_CURRENT_CONTACTS;
  164. return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
  165. } else if (a_is_special && !b_is_special) {
  166. const b_header = HEADER_CURRENT_CONTACTS;
  167. return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
  168. }
  169. }
  170. export function getGroupsAutoCompleteList () {
  171. const roster = /** @type {RosterContacts} */(_converse.state.roster);
  172. const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []);
  173. return [...new Set(groups.filter(i => i))];
  174. }
  175. export function getJIDsAutoCompleteList () {
  176. const roster = /** @type {RosterContacts} */(_converse.state.roster);
  177. return [...new Set(roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))];
  178. }
  179. /**
  180. * @param {string} query
  181. */
  182. export async function getNamesAutoCompleteList (query) {
  183. const options = {
  184. 'mode': /** @type {RequestMode} */('cors'),
  185. 'headers': {
  186. 'Accept': 'text/json'
  187. }
  188. };
  189. const url = `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(query)}`;
  190. let response;
  191. try {
  192. response = await fetch(url, options);
  193. } catch (e) {
  194. log.error(`Failed to fetch names for query "${query}"`);
  195. log.error(e);
  196. return [];
  197. }
  198. const json = response.json;
  199. if (!Array.isArray(json)) {
  200. log.error(`Invalid JSON returned"`);
  201. return [];
  202. }
  203. return json.map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
  204. }