Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
converse.js
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
converse.js
Commits
794a7096
Commit
794a7096
authored
Dec 28, 2020
by
JC Brand
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move converse-rosterview plugin into folder
parent
da131715
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
891 additions
and
855 deletions
+891
-855
src/converse.js
src/converse.js
+1
-1
src/plugins/chatboxviews/index.js
src/plugins/chatboxviews/index.js
+2
-3
src/plugins/rosterview.js
src/plugins/rosterview.js
+0
-848
src/plugins/rosterview/contactview.js
src/plugins/rosterview/contactview.js
+236
-0
src/plugins/rosterview/filterview.js
src/plugins/rosterview/filterview.js
+100
-0
src/plugins/rosterview/groupview.js
src/plugins/rosterview/groupview.js
+209
-0
src/plugins/rosterview/index.js
src/plugins/rosterview/index.js
+101
-0
src/plugins/rosterview/rosterview.js
src/plugins/rosterview/rosterview.js
+238
-0
src/plugins/rosterview/templates/group_header.html
src/plugins/rosterview/templates/group_header.html
+0
-0
src/plugins/rosterview/templates/pending_contact.html
src/plugins/rosterview/templates/pending_contact.html
+0
-0
src/plugins/rosterview/templates/requesting_contact.html
src/plugins/rosterview/templates/requesting_contact.html
+0
-0
src/plugins/rosterview/templates/roster.html
src/plugins/rosterview/templates/roster.html
+0
-0
src/plugins/rosterview/templates/roster_filter.js
src/plugins/rosterview/templates/roster_filter.js
+0
-0
src/plugins/rosterview/templates/roster_item.html
src/plugins/rosterview/templates/roster_item.html
+0
-0
src/shared/avatar.js
src/shared/avatar.js
+4
-3
No files found.
src/converse.js
View file @
794a7096
...
...
@@ -29,7 +29,7 @@ import "./plugins/profile.js";
import
"
./plugins/push.js
"
;
// XEP-0357 Push Notifications
import
"
./plugins/register.js
"
;
// XEP-0077 In-band registration
import
"
./plugins/roomslist/index.js
"
;
// Show currently open chat rooms
import
"
./plugins/rosterview.js
"
;
import
"
./plugins/rosterview
/index
.js
"
;
import
"
./plugins/singleton.js
"
;
/* END: Removable components */
...
...
src/plugins/chatboxviews/index.js
View file @
794a7096
...
...
@@ -5,9 +5,8 @@
*/
import
'
@converse/headless/plugins/chatboxes
'
;
import
'
components/converse.js
'
;
import
AvatarMixin
from
'
shared/avatar.js
'
;
import
ViewWithAvatar
from
'
shared/avatar.js
'
;
import
ChatBoxViews
from
'
./view.js
'
;
import
{
View
}
from
'
@converse/skeletor/src/view
'
;
import
{
_converse
,
api
,
converse
}
from
'
@converse/headless/core
'
;
function
onChatBoxViewsInitialized
()
{
...
...
@@ -47,7 +46,7 @@ converse.plugins.add('converse-chatboxviews', {
'
theme
'
:
'
default
'
});
_converse
.
ViewWithAvatar
=
View
.
extend
(
AvatarMixin
)
;
_converse
.
ViewWithAvatar
=
View
WithAvatar
;
_converse
.
ChatBoxViews
=
ChatBoxViews
;
/************************ BEGIN Event Handlers ************************/
...
...
src/plugins/rosterview.js
deleted
100644 → 0
View file @
da131715
/**
* @module converse-rosterview
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import
"
./modal
"
;
import
"
@converse/headless/plugins/chatboxes
"
;
import
"
@converse/headless/plugins/roster
"
;
import
"
../modals/add-contact.js
"
;
import
log
from
"
@converse/headless/log
"
;
import
tpl_group_header
from
"
../templates/group_header.html
"
;
import
tpl_pending_contact
from
"
../templates/pending_contact.html
"
;
import
tpl_requesting_contact
from
"
../templates/requesting_contact.html
"
;
import
tpl_roster
from
"
../templates/roster.html
"
;
import
tpl_roster_filter
from
"
../templates/roster_filter.js
"
;
import
tpl_roster_item
from
"
../templates/roster_item.html
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
OrderedListView
}
from
"
@converse/skeletor/src/overview
"
;
import
{
View
}
from
'
@converse/skeletor/src/view.js
'
;
import
{
__
}
from
'
../i18n
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
{
debounce
,
has
,
without
}
from
"
lodash-es
"
;
const
u
=
converse
.
env
.
utils
;
converse
.
plugins
.
add
(
'
converse-rosterview
'
,
{
dependencies
:
[
"
converse-roster
"
,
"
converse-modal
"
,
"
converse-chatboxviews
"
],
initialize
()
{
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api
.
settings
.
extend
({
'
autocomplete_add_contact
'
:
true
,
'
allow_chat_pending_contacts
'
:
true
,
'
allow_contact_removal
'
:
true
,
'
hide_offline_users
'
:
false
,
'
roster_groups
'
:
true
,
'
xhr_user_search_url
'
:
null
,
});
api
.
promises
.
add
(
'
rosterViewInitialized
'
);
const
STATUSES
=
{
'
dnd
'
:
__
(
'
This contact is busy
'
),
'
online
'
:
__
(
'
This contact is online
'
),
'
offline
'
:
__
(
'
This contact is offline
'
),
'
unavailable
'
:
__
(
'
This contact is unavailable
'
),
'
xa
'
:
__
(
'
This contact is away for an extended period
'
),
'
away
'
:
__
(
'
This contact is away
'
)
};
_converse
.
RosterFilter
=
Model
.
extend
({
initialize
()
{
this
.
set
({
'
filter_text
'
:
''
,
'
filter_type
'
:
'
contacts
'
,
'
chat_state
'
:
'
online
'
});
},
});
_converse
.
RosterFilterView
=
View
.
extend
({
tagName
:
'
span
'
,
initialize
()
{
this
.
listenTo
(
this
.
model
,
'
change:filter_type
'
,
this
.
render
);
this
.
listenTo
(
this
.
model
,
'
change:filter_text
'
,
this
.
render
);
},
toHTML
()
{
return
tpl_roster_filter
(
Object
.
assign
(
this
.
model
.
toJSON
(),
{
visible
:
this
.
shouldBeVisible
(),
placeholder
:
__
(
'
Filter
'
),
title_contact_filter
:
__
(
'
Filter by contact name
'
),
title_group_filter
:
__
(
'
Filter by group name
'
),
title_status_filter
:
__
(
'
Filter by status
'
),
label_any
:
__
(
'
Any
'
),
label_unread_messages
:
__
(
'
Unread
'
),
label_online
:
__
(
'
Online
'
),
label_chatty
:
__
(
'
Chatty
'
),
label_busy
:
__
(
'
Busy
'
),
label_away
:
__
(
'
Away
'
),
label_xa
:
__
(
'
Extended Away
'
),
label_offline
:
__
(
'
Offline
'
),
changeChatStateFilter
:
ev
=>
this
.
changeChatStateFilter
(
ev
),
changeTypeFilter
:
ev
=>
this
.
changeTypeFilter
(
ev
),
clearFilter
:
ev
=>
this
.
clearFilter
(
ev
),
liveFilter
:
ev
=>
this
.
liveFilter
(
ev
),
submitFilter
:
ev
=>
this
.
submitFilter
(
ev
),
}));
},
changeChatStateFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
this
.
model
.
save
({
'
chat_state
'
:
this
.
el
.
querySelector
(
'
.state-type
'
).
value
});
},
changeTypeFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
const
type
=
ev
.
target
.
dataset
.
type
;
if
(
type
===
'
state
'
)
{
this
.
model
.
save
({
'
filter_type
'
:
type
,
'
chat_state
'
:
this
.
el
.
querySelector
(
'
.state-type
'
).
value
});
}
else
{
this
.
model
.
save
({
'
filter_type
'
:
type
,
'
filter_text
'
:
this
.
el
.
querySelector
(
'
.roster-filter
'
).
value
});
}
},
liveFilter
:
debounce
(
function
()
{
this
.
model
.
save
({
'
filter_text
'
:
this
.
el
.
querySelector
(
'
.roster-filter
'
).
value
});
},
250
),
submitFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
this
.
liveFilter
();
},
/**
* Returns true if the filter is enabled (i.e. if the user
* has added values to the filter).
* @private
* @method _converse.RosterFilterView#isActive
*/
isActive
()
{
return
(
this
.
model
.
get
(
'
filter_type
'
)
===
'
state
'
||
this
.
model
.
get
(
'
filter_text
'
));
},
shouldBeVisible
()
{
return
_converse
.
roster
&&
_converse
.
roster
.
length
>=
5
||
this
.
isActive
();
},
clearFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
this
.
model
.
save
({
'
filter_text
'
:
''
});
}
});
_converse
.
RosterContactView
=
_converse
.
ViewWithAvatar
.
extend
({
tagName
:
'
li
'
,
className
:
'
list-item d-flex hidden controlbox-padded
'
,
events
:
{
"
click .accept-xmpp-request
"
:
"
acceptRequest
"
,
"
click .decline-xmpp-request
"
:
"
declineRequest
"
,
"
click .open-chat
"
:
"
openChat
"
,
"
click .remove-xmpp-contact
"
:
"
removeContact
"
},
async
initialize
()
{
await
this
.
model
.
initialized
;
this
.
debouncedRender
=
debounce
(
this
.
render
,
50
);
this
.
listenTo
(
this
.
model
,
"
change
"
,
this
.
debouncedRender
);
this
.
listenTo
(
this
.
model
,
"
destroy
"
,
this
.
remove
);
this
.
listenTo
(
this
.
model
,
"
highlight
"
,
this
.
highlight
);
this
.
listenTo
(
this
.
model
,
"
remove
"
,
this
.
remove
);
this
.
listenTo
(
this
.
model
,
'
vcard:change
'
,
this
.
debouncedRender
);
this
.
listenTo
(
this
.
model
.
presence
,
"
change:show
"
,
this
.
debouncedRender
);
this
.
render
();
},
render
()
{
if
(
!
this
.
mayBeShown
())
{
u
.
hideElement
(
this
.
el
);
return
this
;
}
const
ask
=
this
.
model
.
get
(
'
ask
'
),
show
=
this
.
model
.
presence
.
get
(
'
show
'
),
requesting
=
this
.
model
.
get
(
'
requesting
'
),
subscription
=
this
.
model
.
get
(
'
subscription
'
),
jid
=
this
.
model
.
get
(
'
jid
'
);
const
classes_to_remove
=
[
'
current-xmpp-contact
'
,
'
pending-xmpp-contact
'
,
'
requesting-xmpp-contact
'
].
concat
(
Object
.
keys
(
STATUSES
));
classes_to_remove
.
forEach
(
c
=>
u
.
removeClass
(
c
,
this
.
el
));
this
.
el
.
classList
.
add
(
show
);
this
.
el
.
setAttribute
(
'
data-status
'
,
show
);
this
.
highlight
();
if
(
_converse
.
isUniView
())
{
const
chatbox
=
_converse
.
chatboxes
.
get
(
this
.
model
.
get
(
'
jid
'
));
if
(
chatbox
)
{
if
(
chatbox
.
get
(
'
hidden
'
))
{
this
.
el
.
classList
.
remove
(
'
open
'
);
}
else
{
this
.
el
.
classList
.
add
(
'
open
'
);
}
}
}
if
((
ask
===
'
subscribe
'
)
||
(
subscription
===
'
from
'
))
{
/* ask === 'subscribe'
* Means we have asked to subscribe to them.
*
* subscription === 'from'
* They are subscribed to use, but not vice versa.
* We assume that there is a pending subscription
* from us to them (otherwise we're in a state not
* supported by converse.js).
*
* So in both cases the user is a "pending" contact.
*/
const
display_name
=
this
.
model
.
getDisplayName
();
this
.
el
.
classList
.
add
(
'
pending-xmpp-contact
'
);
this
.
el
.
innerHTML
=
tpl_pending_contact
(
Object
.
assign
(
this
.
model
.
toJSON
(),
{
display_name
,
'
desc_remove
'
:
__
(
'
Click to remove %1$s as a contact
'
,
display_name
),
'
allow_chat_pending_contacts
'
:
api
.
settings
.
get
(
'
allow_chat_pending_contacts
'
)
})
);
}
else
if
(
requesting
===
true
)
{
const
display_name
=
this
.
model
.
getDisplayName
();
this
.
el
.
classList
.
add
(
'
requesting-xmpp-contact
'
);
this
.
el
.
innerHTML
=
tpl_requesting_contact
(
Object
.
assign
(
this
.
model
.
toJSON
(),
{
display_name
,
'
desc_accept
'
:
__
(
"
Click to accept the contact request from %1$s
"
,
display_name
),
'
desc_decline
'
:
__
(
"
Click to decline the contact request from %1$s
"
,
display_name
),
'
allow_chat_pending_contacts
'
:
api
.
settings
.
get
(
'
allow_chat_pending_contacts
'
)
})
);
}
else
if
(
subscription
===
'
both
'
||
subscription
===
'
to
'
||
_converse
.
rosterview
.
isSelf
(
jid
))
{
this
.
el
.
classList
.
add
(
'
current-xmpp-contact
'
);
this
.
el
.
classList
.
remove
(
without
([
'
both
'
,
'
to
'
],
subscription
)[
0
]);
this
.
el
.
classList
.
add
(
subscription
);
this
.
renderRosterItem
(
this
.
model
);
}
return
this
;
},
/**
* If appropriate, highlight the contact (by adding the 'open' class).
* @private
* @method _converse.RosterContactView#highlight
*/
highlight
()
{
if
(
_converse
.
isUniView
())
{
const
chatbox
=
_converse
.
chatboxes
.
get
(
this
.
model
.
get
(
'
jid
'
));
if
((
chatbox
&&
chatbox
.
get
(
'
hidden
'
))
||
!
chatbox
)
{
this
.
el
.
classList
.
remove
(
'
open
'
);
}
else
{
this
.
el
.
classList
.
add
(
'
open
'
);
}
}
},
renderRosterItem
(
item
)
{
const
show
=
item
.
presence
.
get
(
'
show
'
)
||
'
offline
'
;
let
status_icon
;
if
(
show
===
'
online
'
)
{
status_icon
=
'
fa fa-circle chat-status chat-status--online
'
;
}
else
if
(
show
===
'
away
'
)
{
status_icon
=
'
fa fa-circle chat-status chat-status--away
'
;
}
else
if
(
show
===
'
xa
'
)
{
status_icon
=
'
far fa-circle chat-status chat-status-xa
'
;
}
else
if
(
show
===
'
dnd
'
)
{
status_icon
=
'
fa fa-minus-circle chat-status chat-status--busy
'
;
}
else
{
status_icon
=
'
fa fa-times-circle chat-status chat-status--offline
'
;
}
const
display_name
=
item
.
getDisplayName
();
this
.
el
.
innerHTML
=
tpl_roster_item
(
Object
.
assign
(
item
.
toJSON
(),
{
show
,
display_name
,
status_icon
,
'
desc_status
'
:
STATUSES
[
show
],
'
desc_chat
'
:
__
(
'
Click to chat with %1$s (XMPP address: %2$s)
'
,
display_name
,
item
.
get
(
'
jid
'
)),
'
desc_remove
'
:
__
(
'
Click to remove %1$s as a contact
'
,
display_name
),
'
allow_contact_removal
'
:
api
.
settings
.
get
(
'
allow_contact_removal
'
),
'
num_unread
'
:
item
.
get
(
'
num_unread
'
)
||
0
,
classes
:
''
})
);
this
.
renderAvatar
();
return
this
;
},
/**
* Returns a boolean indicating whether this contact should
* generally be visible in the roster.
* It doesn't check for the more specific case of whether
* the group it's in is collapsed.
* @private
* @method _converse.RosterContactView#mayBeShown
*/
mayBeShown
()
{
const
chatStatus
=
this
.
model
.
presence
.
get
(
'
show
'
);
if
(
api
.
settings
.
get
(
'
hide_offline_users
'
)
&&
chatStatus
===
'
offline
'
)
{
// If pending or requesting, show
if
((
this
.
model
.
get
(
'
ask
'
)
===
'
subscribe
'
)
||
(
this
.
model
.
get
(
'
subscription
'
)
===
'
from
'
)
||
(
this
.
model
.
get
(
'
requesting
'
)
===
true
))
{
return
true
;
}
return
false
;
}
return
true
;
},
openChat
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
this
.
model
.
openChat
();
},
async
removeContact
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
if
(
!
api
.
settings
.
get
(
'
allow_contact_removal
'
))
{
return
;
}
if
(
!
confirm
(
__
(
"
Are you sure you want to remove this contact?
"
)))
{
return
;
}
try
{
await
this
.
model
.
removeFromRoster
();
this
.
remove
();
if
(
this
.
model
.
collection
)
{
// The model might have already been removed as
// result of a roster push.
this
.
model
.
destroy
();
}
}
catch
(
e
)
{
log
.
error
(
e
);
api
.
alert
(
'
error
'
,
__
(
'
Error
'
),
[
__
(
'
Sorry, there was an error while trying to remove %1$s as a contact.
'
,
this
.
model
.
getDisplayName
())]
);
}
},
async
acceptRequest
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
await
_converse
.
roster
.
sendContactAddIQ
(
this
.
model
.
get
(
'
jid
'
),
this
.
model
.
getFullname
(),
[]
);
this
.
model
.
authorize
().
subscribe
();
},
declineRequest
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
const
result
=
confirm
(
__
(
"
Are you sure you want to decline this contact request?
"
));
if
(
result
===
true
)
{
this
.
model
.
unauthorize
().
destroy
();
}
return
this
;
}
});
/**
* @class
* @namespace _converse.RosterGroupView
* @memberOf _converse
*/
_converse
.
RosterGroupView
=
OrderedListView
.
extend
({
tagName
:
'
div
'
,
className
:
'
roster-group hidden
'
,
events
:
{
"
click a.group-toggle
"
:
"
toggle
"
},
sortImmediatelyOnAdd
:
true
,
ItemView
:
_converse
.
RosterContactView
,
listItems
:
'
model.contacts
'
,
listSelector
:
'
.roster-group-contacts
'
,
sortEvent
:
'
presenceChanged
'
,
initialize
()
{
OrderedListView
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
if
(
this
.
model
.
get
(
'
name
'
)
===
_converse
.
HEADER_UNREAD
)
{
this
.
listenTo
(
this
.
model
.
contacts
,
"
change:num_unread
"
,
c
=>
!
this
.
model
.
get
(
'
unread_messages
'
)
&&
this
.
removeContact
(
c
)
);
}
if
(
this
.
model
.
get
(
'
name
'
)
===
_converse
.
HEADER_REQUESTING_CONTACTS
)
{
this
.
listenTo
(
this
.
model
.
contacts
,
"
change:requesting
"
,
c
=>
!
c
.
get
(
'
requesting
'
)
&&
this
.
removeContact
(
c
)
);
}
if
(
this
.
model
.
get
(
'
name
'
)
===
_converse
.
HEADER_PENDING_CONTACTS
)
{
this
.
listenTo
(
this
.
model
.
contacts
,
"
change:subscription
"
,
c
=>
(
c
.
get
(
'
subscription
'
)
!==
'
from
'
)
&&
this
.
removeContact
(
c
)
);
}
this
.
listenTo
(
this
.
model
.
contacts
,
"
remove
"
,
this
.
onRemove
);
this
.
listenTo
(
_converse
.
roster
,
'
change:groups
'
,
this
.
onContactGroupChange
);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
_converse
.
rosterview
.
on
(
'
rosterContactsFetchedAndProcessed
'
,
()
=>
this
.
sortAndPositionAllItems
()
);
},
render
()
{
this
.
el
.
setAttribute
(
'
data-group
'
,
this
.
model
.
get
(
'
name
'
));
this
.
el
.
innerHTML
=
tpl_group_header
({
'
label_group
'
:
this
.
model
.
get
(
'
name
'
),
'
desc_group_toggle
'
:
this
.
model
.
get
(
'
description
'
),
'
toggle_state
'
:
this
.
model
.
get
(
'
state
'
),
'
_converse
'
:
_converse
});
this
.
contacts_el
=
this
.
el
.
querySelector
(
'
.roster-group-contacts
'
);
return
this
;
},
show
()
{
u
.
showElement
(
this
.
el
);
if
(
this
.
model
.
get
(
'
state
'
)
===
_converse
.
OPENED
)
{
Object
.
values
(
this
.
getAll
())
.
filter
(
v
=>
v
.
mayBeShown
())
.
forEach
(
v
=>
u
.
showElement
(
v
.
el
));
}
return
this
;
},
collapse
()
{
return
u
.
slideIn
(
this
.
contacts_el
);
},
/* Given a list of contacts, make sure they're filtered out
* (aka hidden) and that all other contacts are visible.
* If all contacts are hidden, then also hide the group title.
* @private
* @method _converse.RosterGroupView#filterOutContacts
* @param { Array } contacts
*/
filterOutContacts
(
contacts
=
[])
{
let
shown
=
0
;
this
.
model
.
contacts
.
forEach
(
contact
=>
{
const
contact_view
=
this
.
get
(
contact
.
get
(
'
id
'
));
if
(
contacts
.
includes
(
contact
))
{
u
.
hideElement
(
contact_view
.
el
);
}
else
if
(
contact_view
.
mayBeShown
())
{
u
.
showElement
(
contact_view
.
el
);
shown
+=
1
;
}
});
if
(
shown
)
{
u
.
showElement
(
this
.
el
);
}
else
{
u
.
hideElement
(
this
.
el
);
}
},
/**
* Given the filter query "q" and the filter type "type",
* return a list of contacts that need to be filtered out.
* @private
* @method _converse.RosterGroupView#getFilterMatches
* @param { String } q - The filter query
* @param { String } type - The filter type
*/
getFilterMatches
(
q
,
type
)
{
if
(
q
.
length
===
0
)
{
return
[];
}
q
=
q
.
toLowerCase
();
const
contacts
=
this
.
model
.
contacts
;
if
(
type
===
'
state
'
)
{
const
sticky_groups
=
[
_converse
.
HEADER_REQUESTING_CONTACTS
,
_converse
.
HEADER_UNREAD
];
if
(
sticky_groups
.
includes
(
this
.
model
.
get
(
'
name
'
)))
{
// When filtering by chat state, we still want to
// show sticky groups, even though they don't
// match the state in question.
return
[];
}
else
if
(
q
===
'
unread_messages
'
)
{
return
contacts
.
filter
({
'
num_unread
'
:
0
});
}
else
if
(
q
===
'
online
'
)
{
return
contacts
.
filter
(
c
=>
[
"
offline
"
,
"
unavailable
"
].
includes
(
c
.
presence
.
get
(
'
show
'
)));
}
else
{
return
contacts
.
filter
(
c
=>
!
c
.
presence
.
get
(
'
show
'
).
includes
(
q
));
}
}
else
{
return
contacts
.
filter
(
c
=>
!
c
.
getFilterCriteria
().
includes
(
q
));
}
},
/**
* Filter the group's contacts based on the query "q".
*
* If all contacts are filtered out (i.e. hidden), then the
* group must be filtered out as well.
* @private
* @method _converse.RosterGroupView#filter
* @param { string } q - The query to filter against
* @param { string } type
*/
filter
(
q
,
type
)
{
if
(
q
===
null
||
q
===
undefined
)
{
type
=
type
||
_converse
.
rosterview
.
filter_view
.
model
.
get
(
'
filter_type
'
);
if
(
type
===
'
state
'
)
{
q
=
_converse
.
rosterview
.
filter_view
.
model
.
get
(
'
chat_state
'
);
}
else
{
q
=
_converse
.
rosterview
.
filter_view
.
model
.
get
(
'
filter_text
'
);
}
}
this
.
filterOutContacts
(
this
.
getFilterMatches
(
q
,
type
));
},
async
toggle
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
const
icon_el
=
ev
.
target
.
matches
(
'
.fa
'
)
?
ev
.
target
:
ev
.
target
.
querySelector
(
'
.fa
'
);
if
(
u
.
hasClass
(
"
fa-caret-down
"
,
icon_el
))
{
this
.
model
.
save
({
state
:
_converse
.
CLOSED
});
await
this
.
collapse
();
icon_el
.
classList
.
remove
(
"
fa-caret-down
"
);
icon_el
.
classList
.
add
(
"
fa-caret-right
"
);
}
else
{
icon_el
.
classList
.
remove
(
"
fa-caret-right
"
);
icon_el
.
classList
.
add
(
"
fa-caret-down
"
);
this
.
model
.
save
({
state
:
_converse
.
OPENED
});
this
.
filter
();
u
.
showElement
(
this
.
el
);
u
.
slideOut
(
this
.
contacts_el
);
}
},
onContactGroupChange
(
contact
)
{
const
in_this_group
=
contact
.
get
(
'
groups
'
).
includes
(
this
.
model
.
get
(
'
name
'
));
const
cid
=
contact
.
get
(
'
id
'
);
const
in_this_overview
=
!
this
.
get
(
cid
);
if
(
in_this_group
&&
!
in_this_overview
)
{
this
.
items
.
trigger
(
'
add
'
,
contact
);
}
else
if
(
!
in_this_group
)
{
this
.
removeContact
(
contact
);
}
},
removeContact
(
contact
)
{
// We suppress events, otherwise the remove event will
// also cause the contact's view to be removed from the
// "Pending Contacts" group.
this
.
model
.
contacts
.
remove
(
contact
,
{
'
silent
'
:
true
});
this
.
onRemove
(
contact
);
},
onRemove
(
contact
)
{
this
.
remove
(
contact
.
get
(
'
jid
'
));
if
(
this
.
model
.
contacts
.
length
===
0
)
{
this
.
remove
();
}
}
});
/**
* @class
* @namespace _converse.RosterView
* @memberOf _converse
*/
_converse
.
RosterView
=
OrderedListView
.
extend
({
tagName
:
'
div
'
,
id
:
'
converse-roster
'
,
className
:
'
controlbox-section
'
,
ItemView
:
_converse
.
RosterGroupView
,
listItems
:
'
model
'
,
listSelector
:
'
.roster-contacts
'
,
sortEvent
:
null
,
// Groups are immutable, so they don't get re-sorted
subviewIndex
:
'
name
'
,
sortImmediatelyOnAdd
:
true
,
events
:
{
'
click a.controlbox-heading__btn.add-contact
'
:
'
showAddContactModal
'
,
'
click a.controlbox-heading__btn.sync-contacts
'
:
'
syncContacts
'
},
initialize
()
{
OrderedListView
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
this
.
listenTo
(
_converse
.
roster
,
"
add
"
,
this
.
onContactAdded
);
this
.
listenTo
(
_converse
.
roster
,
'
change:groups
'
,
this
.
onContactAdded
);
this
.
listenTo
(
_converse
.
roster
,
'
change
'
,
this
.
onContactChange
);
this
.
listenTo
(
_converse
.
roster
,
"
destroy
"
,
this
.
update
);
this
.
listenTo
(
_converse
.
roster
,
"
remove
"
,
this
.
update
);
_converse
.
presences
.
on
(
'
change:show
'
,
()
=>
{
this
.
update
();
this
.
updateFilter
();
});
this
.
listenTo
(
this
.
model
,
"
reset
"
,
this
.
reset
);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
api
.
listen
.
on
(
'
rosterGroupsFetched
'
,
this
.
sortAndPositionAllItems
.
bind
(
this
));
api
.
listen
.
on
(
'
rosterContactsFetched
'
,
()
=>
{
_converse
.
roster
.
each
(
contact
=>
this
.
addRosterContact
(
contact
,
{
'
silent
'
:
true
}));
this
.
update
();
this
.
updateFilter
();
this
.
trigger
(
'
rosterContactsFetchedAndProcessed
'
);
});
this
.
createRosterFilter
();
},
render
()
{
this
.
el
.
innerHTML
=
tpl_roster
({
'
allow_contact_requests
'
:
_converse
.
allow_contact_requests
,
'
heading_contacts
'
:
__
(
'
Contacts
'
),
'
title_add_contact
'
:
__
(
'
Add a contact
'
),
'
title_sync_contacts
'
:
__
(
'
Re-sync your contacts
'
)
});
const
form
=
this
.
el
.
querySelector
(
'
.roster-filter-form
'
);
this
.
el
.
replaceChild
(
this
.
filter_view
.
render
().
el
,
form
);
this
.
roster_el
=
this
.
el
.
querySelector
(
'
.roster-contacts
'
);
return
this
;
},
showAddContactModal
(
ev
)
{
api
.
modal
.
show
(
_converse
.
AddContactModal
,
{
'
model
'
:
new
Model
()},
ev
);
},
createRosterFilter
()
{
// Create a model on which we can store filter properties
const
model
=
new
_converse
.
RosterFilter
();
model
.
id
=
`_converse.rosterfilter-
${
_converse
.
bare_jid
}
`
;
model
.
browserStorage
=
_converse
.
createStore
(
model
.
id
);
this
.
filter_view
=
new
_converse
.
RosterFilterView
({
model
});
this
.
listenTo
(
this
.
filter_view
.
model
,
'
change
'
,
this
.
updateFilter
);
this
.
filter_view
.
model
.
fetch
();
},
/**
* Called whenever the filter settings have been changed or
* when contacts have been added, removed or changed.
*
* Debounced for 100ms so that it doesn't get called for every
* contact fetched from browser storage.
*/
updateFilter
:
debounce
(
function
()
{
const
type
=
this
.
filter_view
.
model
.
get
(
'
filter_type
'
);
if
(
type
===
'
state
'
)
{
this
.
filter
(
this
.
filter_view
.
model
.
get
(
'
chat_state
'
),
type
);
}
else
{
this
.
filter
(
this
.
filter_view
.
model
.
get
(
'
filter_text
'
),
type
);
}
},
100
),
update
()
{
if
(
!
u
.
isVisible
(
this
.
roster_el
))
{
u
.
showElement
(
this
.
roster_el
);
}
this
.
filter_view
.
render
();
return
this
;
},
filter
(
query
,
type
)
{
const
views
=
Object
.
values
(
this
.
getAll
());
// First ensure the filter is restored to its original state
views
.
forEach
(
v
=>
(
v
.
model
.
contacts
.
length
>
0
)
&&
v
.
show
().
filter
(
''
));
// Now we can filter
query
=
query
.
toLowerCase
();
if
(
type
===
'
groups
'
)
{
views
.
forEach
(
view
=>
{
if
(
!
view
.
model
.
get
(
'
name
'
).
toLowerCase
().
includes
(
query
))
{
u
.
slideIn
(
view
.
el
);
}
else
if
(
view
.
model
.
contacts
.
length
>
0
)
{
u
.
slideOut
(
view
.
el
);
}
});
}
else
{
views
.
forEach
(
v
=>
v
.
filter
(
query
,
type
));
}
},
async
syncContacts
(
ev
)
{
ev
.
preventDefault
();
u
.
addClass
(
'
fa-spin
'
,
ev
.
target
);
_converse
.
roster
.
data
.
save
(
'
version
'
,
null
);
await
_converse
.
roster
.
fetchFromServer
();
api
.
user
.
presence
.
send
();
u
.
removeClass
(
'
fa-spin
'
,
ev
.
target
);
},
reset
()
{
this
.
removeAll
();
this
.
render
().
update
();
return
this
;
},
onContactAdded
(
contact
)
{
this
.
addRosterContact
(
contact
)
this
.
update
();
this
.
updateFilter
();
},
onContactChange
(
contact
)
{
this
.
update
();
if
(
has
(
contact
.
changed
,
'
subscription
'
))
{
if
(
contact
.
changed
.
subscription
===
'
from
'
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_PENDING_CONTACTS
);
}
else
if
([
'
both
'
,
'
to
'
].
includes
(
contact
.
get
(
'
subscription
'
)))
{
this
.
addExistingContact
(
contact
);
}
}
if
(
has
(
contact
.
changed
,
'
num_unread
'
)
&&
contact
.
get
(
'
num_unread
'
))
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_UNREAD
);
}
if
(
has
(
contact
.
changed
,
'
ask
'
)
&&
contact
.
changed
.
ask
===
'
subscribe
'
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_PENDING_CONTACTS
);
}
if
(
has
(
contact
.
changed
,
'
subscription
'
)
&&
contact
.
changed
.
requesting
===
'
true
'
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_REQUESTING_CONTACTS
);
}
this
.
updateFilter
();
},
/**
* Returns the group as specified by name.
* Creates the group if it doesn't exist.
* @method _converse.RosterView#getGroup
* @private
* @param {string} name
*/
getGroup
(
name
)
{
const
view
=
this
.
get
(
name
);
if
(
view
)
{
return
view
.
model
;
}
return
this
.
model
.
create
({
name
});
},
addContactToGroup
(
contact
,
name
,
options
)
{
this
.
getGroup
(
name
).
contacts
.
add
(
contact
,
options
);
this
.
sortAndPositionAllItems
();
},
addExistingContact
(
contact
,
options
)
{
let
groups
;
if
(
api
.
settings
.
get
(
'
roster_groups
'
))
{
groups
=
contact
.
get
(
'
groups
'
);
groups
=
(
groups
.
length
===
0
)
?
[
_converse
.
HEADER_UNGROUPED
]
:
groups
;
}
else
{
groups
=
[
_converse
.
HEADER_CURRENT_CONTACTS
];
}
if
(
contact
.
get
(
'
num_unread
'
))
{
groups
.
push
(
_converse
.
HEADER_UNREAD
);
}
groups
.
forEach
(
g
=>
this
.
addContactToGroup
(
contact
,
g
,
options
));
},
isSelf
(
jid
)
{
return
u
.
isSameBareJID
(
jid
,
_converse
.
connection
.
jid
);
},
addRosterContact
(
contact
,
options
)
{
const
jid
=
contact
.
get
(
'
jid
'
);
if
(
contact
.
get
(
'
subscription
'
)
===
'
both
'
||
contact
.
get
(
'
subscription
'
)
===
'
to
'
||
this
.
isSelf
(
jid
))
{
this
.
addExistingContact
(
contact
,
options
);
}
else
{
if
(
!
_converse
.
allow_contact_requests
)
{
log
.
debug
(
`Not adding requesting or pending contact
${
jid
}
`
+
`because allow_contact_requests is false`
);
return
;
}
if
((
contact
.
get
(
'
ask
'
)
===
'
subscribe
'
)
||
(
contact
.
get
(
'
subscription
'
)
===
'
from
'
))
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_PENDING_CONTACTS
,
options
);
}
else
if
(
contact
.
get
(
'
requesting
'
)
===
true
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_REQUESTING_CONTACTS
,
options
);
}
}
return
this
;
}
});
/* -------- Event Handlers ----------- */
api
.
listen
.
on
(
'
chatBoxesInitialized
'
,
()
=>
{
function
highlightRosterItem
(
chatbox
)
{
const
contact
=
_converse
.
roster
&&
_converse
.
roster
.
findWhere
({
'
jid
'
:
chatbox
.
get
(
'
jid
'
)});
if
(
contact
!==
undefined
)
{
contact
.
trigger
(
'
highlight
'
);
}
}
_converse
.
chatboxes
.
on
(
'
destroy
'
,
chatbox
=>
highlightRosterItem
(
chatbox
));
_converse
.
chatboxes
.
on
(
'
change:hidden
'
,
chatbox
=>
highlightRosterItem
(
chatbox
));
});
api
.
listen
.
on
(
'
controlBoxInitialized
'
,
(
view
)
=>
{
function
insertRoster
()
{
if
(
!
view
.
model
.
get
(
'
connected
'
)
||
api
.
settings
.
get
(
"
authentication
"
)
===
_converse
.
ANONYMOUS
)
{
return
;
}
/* Place the rosterview inside the "Contacts" panel. */
api
.
waitUntil
(
'
rosterViewInitialized
'
)
.
then
(()
=>
view
.
controlbox_pane
.
el
.
insertAdjacentElement
(
'
beforeEnd
'
,
_converse
.
rosterview
.
el
))
.
catch
(
e
=>
log
.
fatal
(
e
));
}
insertRoster
();
view
.
model
.
on
(
'
change:connected
'
,
insertRoster
);
});
function
initRosterView
()
{
/* Create an instance of RosterView once the RosterGroups
* collection has been created (in @converse/headless/core.js)
*/
if
(
api
.
settings
.
get
(
"
authentication
"
)
===
_converse
.
ANONYMOUS
)
{
return
;
}
_converse
.
rosterview
=
new
_converse
.
RosterView
({
'
model
'
:
_converse
.
rostergroups
});
_converse
.
rosterview
.
render
();
/**
* Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized
* @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
*/
api
.
trigger
(
'
rosterViewInitialized
'
);
}
api
.
listen
.
on
(
'
rosterInitialized
'
,
initRosterView
);
api
.
listen
.
on
(
'
rosterReadyAfterReconnection
'
,
initRosterView
);
api
.
listen
.
on
(
'
afterTearDown
'
,
()
=>
{
if
(
converse
.
rosterview
)
{
converse
.
rosterview
.
model
.
off
().
reset
();
converse
.
rosterview
.
each
(
groupview
=>
groupview
.
removeAll
().
remove
());
converse
.
rosterview
.
removeAll
().
remove
();
delete
converse
.
rosterview
;
}
});
}
});
src/plugins/rosterview/contactview.js
0 → 100644
View file @
794a7096
import
ViewWithAvatar
from
'
shared/avatar.js
'
;
import
log
from
"
@converse/headless/log
"
;
import
tpl_pending_contact
from
"
./templates/pending_contact.html
"
;
import
tpl_requesting_contact
from
"
./templates/requesting_contact.html
"
;
import
tpl_roster_item
from
"
./templates/roster_item.html
"
;
import
{
__
}
from
'
i18n
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
{
debounce
,
without
}
from
"
lodash-es
"
;
const
u
=
converse
.
env
.
utils
;
const
STATUSES
=
{
'
dnd
'
:
__
(
'
This contact is busy
'
),
'
online
'
:
__
(
'
This contact is online
'
),
'
offline
'
:
__
(
'
This contact is offline
'
),
'
unavailable
'
:
__
(
'
This contact is unavailable
'
),
'
xa
'
:
__
(
'
This contact is away for an extended period
'
),
'
away
'
:
__
(
'
This contact is away
'
)
};
const
RosterContactView
=
ViewWithAvatar
.
extend
({
tagName
:
'
li
'
,
className
:
'
list-item d-flex hidden controlbox-padded
'
,
events
:
{
"
click .accept-xmpp-request
"
:
"
acceptRequest
"
,
"
click .decline-xmpp-request
"
:
"
declineRequest
"
,
"
click .open-chat
"
:
"
openChat
"
,
"
click .remove-xmpp-contact
"
:
"
removeContact
"
},
async
initialize
()
{
await
this
.
model
.
initialized
;
this
.
debouncedRender
=
debounce
(
this
.
render
,
50
);
this
.
listenTo
(
this
.
model
,
"
change
"
,
this
.
debouncedRender
);
this
.
listenTo
(
this
.
model
,
"
destroy
"
,
this
.
remove
);
this
.
listenTo
(
this
.
model
,
"
highlight
"
,
this
.
highlight
);
this
.
listenTo
(
this
.
model
,
"
remove
"
,
this
.
remove
);
this
.
listenTo
(
this
.
model
,
'
vcard:change
'
,
this
.
debouncedRender
);
this
.
listenTo
(
this
.
model
.
presence
,
"
change:show
"
,
this
.
debouncedRender
);
this
.
render
();
},
render
()
{
if
(
!
this
.
mayBeShown
())
{
u
.
hideElement
(
this
.
el
);
return
this
;
}
const
ask
=
this
.
model
.
get
(
'
ask
'
),
show
=
this
.
model
.
presence
.
get
(
'
show
'
),
requesting
=
this
.
model
.
get
(
'
requesting
'
),
subscription
=
this
.
model
.
get
(
'
subscription
'
),
jid
=
this
.
model
.
get
(
'
jid
'
);
const
classes_to_remove
=
[
'
current-xmpp-contact
'
,
'
pending-xmpp-contact
'
,
'
requesting-xmpp-contact
'
].
concat
(
Object
.
keys
(
STATUSES
));
classes_to_remove
.
forEach
(
c
=>
u
.
removeClass
(
c
,
this
.
el
));
this
.
el
.
classList
.
add
(
show
);
this
.
el
.
setAttribute
(
'
data-status
'
,
show
);
this
.
highlight
();
if
(
_converse
.
isUniView
())
{
const
chatbox
=
_converse
.
chatboxes
.
get
(
this
.
model
.
get
(
'
jid
'
));
if
(
chatbox
)
{
if
(
chatbox
.
get
(
'
hidden
'
))
{
this
.
el
.
classList
.
remove
(
'
open
'
);
}
else
{
this
.
el
.
classList
.
add
(
'
open
'
);
}
}
}
if
((
ask
===
'
subscribe
'
)
||
(
subscription
===
'
from
'
))
{
/* ask === 'subscribe'
* Means we have asked to subscribe to them.
*
* subscription === 'from'
* They are subscribed to use, but not vice versa.
* We assume that there is a pending subscription
* from us to them (otherwise we're in a state not
* supported by converse.js).
*
* So in both cases the user is a "pending" contact.
*/
const
display_name
=
this
.
model
.
getDisplayName
();
this
.
el
.
classList
.
add
(
'
pending-xmpp-contact
'
);
this
.
el
.
innerHTML
=
tpl_pending_contact
(
Object
.
assign
(
this
.
model
.
toJSON
(),
{
display_name
,
'
desc_remove
'
:
__
(
'
Click to remove %1$s as a contact
'
,
display_name
),
'
allow_chat_pending_contacts
'
:
api
.
settings
.
get
(
'
allow_chat_pending_contacts
'
)
})
);
}
else
if
(
requesting
===
true
)
{
const
display_name
=
this
.
model
.
getDisplayName
();
this
.
el
.
classList
.
add
(
'
requesting-xmpp-contact
'
);
this
.
el
.
innerHTML
=
tpl_requesting_contact
(
Object
.
assign
(
this
.
model
.
toJSON
(),
{
display_name
,
'
desc_accept
'
:
__
(
"
Click to accept the contact request from %1$s
"
,
display_name
),
'
desc_decline
'
:
__
(
"
Click to decline the contact request from %1$s
"
,
display_name
),
'
allow_chat_pending_contacts
'
:
api
.
settings
.
get
(
'
allow_chat_pending_contacts
'
)
})
);
}
else
if
(
subscription
===
'
both
'
||
subscription
===
'
to
'
||
_converse
.
rosterview
.
isSelf
(
jid
))
{
this
.
el
.
classList
.
add
(
'
current-xmpp-contact
'
);
this
.
el
.
classList
.
remove
(
without
([
'
both
'
,
'
to
'
],
subscription
)[
0
]);
this
.
el
.
classList
.
add
(
subscription
);
this
.
renderRosterItem
(
this
.
model
);
}
return
this
;
},
/**
* If appropriate, highlight the contact (by adding the 'open' class).
* @private
* @method _converse.RosterContactView#highlight
*/
highlight
()
{
if
(
_converse
.
isUniView
())
{
const
chatbox
=
_converse
.
chatboxes
.
get
(
this
.
model
.
get
(
'
jid
'
));
if
((
chatbox
&&
chatbox
.
get
(
'
hidden
'
))
||
!
chatbox
)
{
this
.
el
.
classList
.
remove
(
'
open
'
);
}
else
{
this
.
el
.
classList
.
add
(
'
open
'
);
}
}
},
renderRosterItem
(
item
)
{
const
show
=
item
.
presence
.
get
(
'
show
'
)
||
'
offline
'
;
let
status_icon
;
if
(
show
===
'
online
'
)
{
status_icon
=
'
fa fa-circle chat-status chat-status--online
'
;
}
else
if
(
show
===
'
away
'
)
{
status_icon
=
'
fa fa-circle chat-status chat-status--away
'
;
}
else
if
(
show
===
'
xa
'
)
{
status_icon
=
'
far fa-circle chat-status chat-status-xa
'
;
}
else
if
(
show
===
'
dnd
'
)
{
status_icon
=
'
fa fa-minus-circle chat-status chat-status--busy
'
;
}
else
{
status_icon
=
'
fa fa-times-circle chat-status chat-status--offline
'
;
}
const
display_name
=
item
.
getDisplayName
();
this
.
el
.
innerHTML
=
tpl_roster_item
(
Object
.
assign
(
item
.
toJSON
(),
{
show
,
display_name
,
status_icon
,
'
desc_status
'
:
STATUSES
[
show
],
'
desc_chat
'
:
__
(
'
Click to chat with %1$s (XMPP address: %2$s)
'
,
display_name
,
item
.
get
(
'
jid
'
)),
'
desc_remove
'
:
__
(
'
Click to remove %1$s as a contact
'
,
display_name
),
'
allow_contact_removal
'
:
api
.
settings
.
get
(
'
allow_contact_removal
'
),
'
num_unread
'
:
item
.
get
(
'
num_unread
'
)
||
0
,
classes
:
''
})
);
this
.
renderAvatar
();
return
this
;
},
/**
* Returns a boolean indicating whether this contact should
* generally be visible in the roster.
* It doesn't check for the more specific case of whether
* the group it's in is collapsed.
* @private
* @method _converse.RosterContactView#mayBeShown
*/
mayBeShown
()
{
const
chatStatus
=
this
.
model
.
presence
.
get
(
'
show
'
);
if
(
api
.
settings
.
get
(
'
hide_offline_users
'
)
&&
chatStatus
===
'
offline
'
)
{
// If pending or requesting, show
if
((
this
.
model
.
get
(
'
ask
'
)
===
'
subscribe
'
)
||
(
this
.
model
.
get
(
'
subscription
'
)
===
'
from
'
)
||
(
this
.
model
.
get
(
'
requesting
'
)
===
true
))
{
return
true
;
}
return
false
;
}
return
true
;
},
openChat
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
this
.
model
.
openChat
();
},
async
removeContact
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
if
(
!
api
.
settings
.
get
(
'
allow_contact_removal
'
))
{
return
;
}
if
(
!
confirm
(
__
(
"
Are you sure you want to remove this contact?
"
)))
{
return
;
}
try
{
await
this
.
model
.
removeFromRoster
();
this
.
remove
();
if
(
this
.
model
.
collection
)
{
// The model might have already been removed as
// result of a roster push.
this
.
model
.
destroy
();
}
}
catch
(
e
)
{
log
.
error
(
e
);
api
.
alert
(
'
error
'
,
__
(
'
Error
'
),
[
__
(
'
Sorry, there was an error while trying to remove %1$s as a contact.
'
,
this
.
model
.
getDisplayName
())]
);
}
},
async
acceptRequest
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
await
_converse
.
roster
.
sendContactAddIQ
(
this
.
model
.
get
(
'
jid
'
),
this
.
model
.
getFullname
(),
[]
);
this
.
model
.
authorize
().
subscribe
();
},
declineRequest
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
const
result
=
confirm
(
__
(
"
Are you sure you want to decline this contact request?
"
));
if
(
result
===
true
)
{
this
.
model
.
unauthorize
().
destroy
();
}
return
this
;
}
});
export
default
RosterContactView
;
src/plugins/rosterview/filterview.js
0 → 100644
View file @
794a7096
import
tpl_roster_filter
from
"
./templates/roster_filter.js
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
View
}
from
'
@converse/skeletor/src/view.js
'
;
import
{
__
}
from
'
i18n
'
;
import
{
_converse
}
from
"
@converse/headless/core
"
;
import
{
debounce
}
from
"
lodash-es
"
;
export
const
RosterFilter
=
Model
.
extend
({
initialize
()
{
this
.
set
({
'
filter_text
'
:
''
,
'
filter_type
'
:
'
contacts
'
,
'
chat_state
'
:
'
online
'
});
},
});
export
const
RosterFilterView
=
View
.
extend
({
tagName
:
'
span
'
,
initialize
()
{
this
.
listenTo
(
this
.
model
,
'
change:filter_type
'
,
this
.
render
);
this
.
listenTo
(
this
.
model
,
'
change:filter_text
'
,
this
.
render
);
},
toHTML
()
{
return
tpl_roster_filter
(
Object
.
assign
(
this
.
model
.
toJSON
(),
{
visible
:
this
.
shouldBeVisible
(),
placeholder
:
__
(
'
Filter
'
),
title_contact_filter
:
__
(
'
Filter by contact name
'
),
title_group_filter
:
__
(
'
Filter by group name
'
),
title_status_filter
:
__
(
'
Filter by status
'
),
label_any
:
__
(
'
Any
'
),
label_unread_messages
:
__
(
'
Unread
'
),
label_online
:
__
(
'
Online
'
),
label_chatty
:
__
(
'
Chatty
'
),
label_busy
:
__
(
'
Busy
'
),
label_away
:
__
(
'
Away
'
),
label_xa
:
__
(
'
Extended Away
'
),
label_offline
:
__
(
'
Offline
'
),
changeChatStateFilter
:
ev
=>
this
.
changeChatStateFilter
(
ev
),
changeTypeFilter
:
ev
=>
this
.
changeTypeFilter
(
ev
),
clearFilter
:
ev
=>
this
.
clearFilter
(
ev
),
liveFilter
:
ev
=>
this
.
liveFilter
(
ev
),
submitFilter
:
ev
=>
this
.
submitFilter
(
ev
),
}));
},
changeChatStateFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
this
.
model
.
save
({
'
chat_state
'
:
this
.
el
.
querySelector
(
'
.state-type
'
).
value
});
},
changeTypeFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
const
type
=
ev
.
target
.
dataset
.
type
;
if
(
type
===
'
state
'
)
{
this
.
model
.
save
({
'
filter_type
'
:
type
,
'
chat_state
'
:
this
.
el
.
querySelector
(
'
.state-type
'
).
value
});
}
else
{
this
.
model
.
save
({
'
filter_type
'
:
type
,
'
filter_text
'
:
this
.
el
.
querySelector
(
'
.roster-filter
'
).
value
});
}
},
liveFilter
:
debounce
(
function
()
{
this
.
model
.
save
({
'
filter_text
'
:
this
.
el
.
querySelector
(
'
.roster-filter
'
).
value
});
},
250
),
submitFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
this
.
liveFilter
();
},
/**
* Returns true if the filter is enabled (i.e. if the user
* has added values to the filter).
* @private
* @method _converse.RosterFilterView#isActive
*/
isActive
()
{
return
(
this
.
model
.
get
(
'
filter_type
'
)
===
'
state
'
||
this
.
model
.
get
(
'
filter_text
'
));
},
shouldBeVisible
()
{
return
_converse
.
roster
&&
_converse
.
roster
.
length
>=
5
||
this
.
isActive
();
},
clearFilter
(
ev
)
{
ev
&&
ev
.
preventDefault
();
this
.
model
.
save
({
'
filter_text
'
:
''
});
}
});
src/plugins/rosterview/groupview.js
0 → 100644
View file @
794a7096
import
RosterContactView
from
'
./contactview.js
'
;
import
tpl_group_header
from
"
./templates/group_header.html
"
;
import
{
OrderedListView
}
from
"
@converse/skeletor/src/overview
"
;
import
{
_converse
,
converse
}
from
"
@converse/headless/core
"
;
const
u
=
converse
.
env
.
utils
;
/**
* @class
* @namespace _converse.RosterGroupView
* @memberOf _converse
*/
const
RosterGroupView
=
OrderedListView
.
extend
({
tagName
:
'
div
'
,
className
:
'
roster-group hidden
'
,
events
:
{
"
click a.group-toggle
"
:
"
toggle
"
},
sortImmediatelyOnAdd
:
true
,
ItemView
:
RosterContactView
,
listItems
:
'
model.contacts
'
,
listSelector
:
'
.roster-group-contacts
'
,
sortEvent
:
'
presenceChanged
'
,
initialize
()
{
OrderedListView
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
if
(
this
.
model
.
get
(
'
name
'
)
===
_converse
.
HEADER_UNREAD
)
{
this
.
listenTo
(
this
.
model
.
contacts
,
"
change:num_unread
"
,
c
=>
!
this
.
model
.
get
(
'
unread_messages
'
)
&&
this
.
removeContact
(
c
)
);
}
if
(
this
.
model
.
get
(
'
name
'
)
===
_converse
.
HEADER_REQUESTING_CONTACTS
)
{
this
.
listenTo
(
this
.
model
.
contacts
,
"
change:requesting
"
,
c
=>
!
c
.
get
(
'
requesting
'
)
&&
this
.
removeContact
(
c
)
);
}
if
(
this
.
model
.
get
(
'
name
'
)
===
_converse
.
HEADER_PENDING_CONTACTS
)
{
this
.
listenTo
(
this
.
model
.
contacts
,
"
change:subscription
"
,
c
=>
(
c
.
get
(
'
subscription
'
)
!==
'
from
'
)
&&
this
.
removeContact
(
c
)
);
}
this
.
listenTo
(
this
.
model
.
contacts
,
"
remove
"
,
this
.
onRemove
);
this
.
listenTo
(
_converse
.
roster
,
'
change:groups
'
,
this
.
onContactGroupChange
);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
_converse
.
rosterview
.
on
(
'
rosterContactsFetchedAndProcessed
'
,
()
=>
this
.
sortAndPositionAllItems
()
);
},
render
()
{
this
.
el
.
setAttribute
(
'
data-group
'
,
this
.
model
.
get
(
'
name
'
));
this
.
el
.
innerHTML
=
tpl_group_header
({
'
label_group
'
:
this
.
model
.
get
(
'
name
'
),
'
desc_group_toggle
'
:
this
.
model
.
get
(
'
description
'
),
'
toggle_state
'
:
this
.
model
.
get
(
'
state
'
),
'
_converse
'
:
_converse
});
this
.
contacts_el
=
this
.
el
.
querySelector
(
'
.roster-group-contacts
'
);
return
this
;
},
show
()
{
u
.
showElement
(
this
.
el
);
if
(
this
.
model
.
get
(
'
state
'
)
===
_converse
.
OPENED
)
{
Object
.
values
(
this
.
getAll
())
.
filter
(
v
=>
v
.
mayBeShown
())
.
forEach
(
v
=>
u
.
showElement
(
v
.
el
));
}
return
this
;
},
collapse
()
{
return
u
.
slideIn
(
this
.
contacts_el
);
},
/* Given a list of contacts, make sure they're filtered out
* (aka hidden) and that all other contacts are visible.
* If all contacts are hidden, then also hide the group title.
* @private
* @method _converse.RosterGroupView#filterOutContacts
* @param { Array } contacts
*/
filterOutContacts
(
contacts
=
[])
{
let
shown
=
0
;
this
.
model
.
contacts
.
forEach
(
contact
=>
{
const
contact_view
=
this
.
get
(
contact
.
get
(
'
id
'
));
if
(
contacts
.
includes
(
contact
))
{
u
.
hideElement
(
contact_view
.
el
);
}
else
if
(
contact_view
.
mayBeShown
())
{
u
.
showElement
(
contact_view
.
el
);
shown
+=
1
;
}
});
if
(
shown
)
{
u
.
showElement
(
this
.
el
);
}
else
{
u
.
hideElement
(
this
.
el
);
}
},
/**
* Given the filter query "q" and the filter type "type",
* return a list of contacts that need to be filtered out.
* @private
* @method _converse.RosterGroupView#getFilterMatches
* @param { String } q - The filter query
* @param { String } type - The filter type
*/
getFilterMatches
(
q
,
type
)
{
if
(
q
.
length
===
0
)
{
return
[];
}
q
=
q
.
toLowerCase
();
const
contacts
=
this
.
model
.
contacts
;
if
(
type
===
'
state
'
)
{
const
sticky_groups
=
[
_converse
.
HEADER_REQUESTING_CONTACTS
,
_converse
.
HEADER_UNREAD
];
if
(
sticky_groups
.
includes
(
this
.
model
.
get
(
'
name
'
)))
{
// When filtering by chat state, we still want to
// show sticky groups, even though they don't
// match the state in question.
return
[];
}
else
if
(
q
===
'
unread_messages
'
)
{
return
contacts
.
filter
({
'
num_unread
'
:
0
});
}
else
if
(
q
===
'
online
'
)
{
return
contacts
.
filter
(
c
=>
[
"
offline
"
,
"
unavailable
"
].
includes
(
c
.
presence
.
get
(
'
show
'
)));
}
else
{
return
contacts
.
filter
(
c
=>
!
c
.
presence
.
get
(
'
show
'
).
includes
(
q
));
}
}
else
{
return
contacts
.
filter
(
c
=>
!
c
.
getFilterCriteria
().
includes
(
q
));
}
},
/**
* Filter the group's contacts based on the query "q".
*
* If all contacts are filtered out (i.e. hidden), then the
* group must be filtered out as well.
* @private
* @method _converse.RosterGroupView#filter
* @param { string } q - The query to filter against
* @param { string } type
*/
filter
(
q
,
type
)
{
if
(
q
===
null
||
q
===
undefined
)
{
type
=
type
||
_converse
.
rosterview
.
filter_view
.
model
.
get
(
'
filter_type
'
);
if
(
type
===
'
state
'
)
{
q
=
_converse
.
rosterview
.
filter_view
.
model
.
get
(
'
chat_state
'
);
}
else
{
q
=
_converse
.
rosterview
.
filter_view
.
model
.
get
(
'
filter_text
'
);
}
}
this
.
filterOutContacts
(
this
.
getFilterMatches
(
q
,
type
));
},
async
toggle
(
ev
)
{
if
(
ev
&&
ev
.
preventDefault
)
{
ev
.
preventDefault
();
}
const
icon_el
=
ev
.
target
.
matches
(
'
.fa
'
)
?
ev
.
target
:
ev
.
target
.
querySelector
(
'
.fa
'
);
if
(
u
.
hasClass
(
"
fa-caret-down
"
,
icon_el
))
{
this
.
model
.
save
({
state
:
_converse
.
CLOSED
});
await
this
.
collapse
();
icon_el
.
classList
.
remove
(
"
fa-caret-down
"
);
icon_el
.
classList
.
add
(
"
fa-caret-right
"
);
}
else
{
icon_el
.
classList
.
remove
(
"
fa-caret-right
"
);
icon_el
.
classList
.
add
(
"
fa-caret-down
"
);
this
.
model
.
save
({
state
:
_converse
.
OPENED
});
this
.
filter
();
u
.
showElement
(
this
.
el
);
u
.
slideOut
(
this
.
contacts_el
);
}
},
onContactGroupChange
(
contact
)
{
const
in_this_group
=
contact
.
get
(
'
groups
'
).
includes
(
this
.
model
.
get
(
'
name
'
));
const
cid
=
contact
.
get
(
'
id
'
);
const
in_this_overview
=
!
this
.
get
(
cid
);
if
(
in_this_group
&&
!
in_this_overview
)
{
this
.
items
.
trigger
(
'
add
'
,
contact
);
}
else
if
(
!
in_this_group
)
{
this
.
removeContact
(
contact
);
}
},
removeContact
(
contact
)
{
// We suppress events, otherwise the remove event will
// also cause the contact's view to be removed from the
// "Pending Contacts" group.
this
.
model
.
contacts
.
remove
(
contact
,
{
'
silent
'
:
true
});
this
.
onRemove
(
contact
);
},
onRemove
(
contact
)
{
this
.
remove
(
contact
.
get
(
'
jid
'
));
if
(
this
.
model
.
contacts
.
length
===
0
)
{
this
.
remove
();
}
}
});
export
default
RosterGroupView
;
src/plugins/rosterview/index.js
0 → 100644
View file @
794a7096
/**
* @module converse-rosterview
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import
"
../modal
"
;
import
"
@converse/headless/plugins/chatboxes
"
;
import
"
@converse/headless/plugins/roster
"
;
import
"
modals/add-contact.js
"
;
import
RosterContactView
from
'
./contactview.js
'
;
import
RosterGroupView
from
'
./groupview.js
'
;
import
RosterView
from
'
./rosterview.js
'
;
import
log
from
"
@converse/headless/log
"
;
import
{
RosterFilter
,
RosterFilterView
}
from
'
./filterview.js
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
converse
.
plugins
.
add
(
'
converse-rosterview
'
,
{
dependencies
:
[
"
converse-roster
"
,
"
converse-modal
"
,
"
converse-chatboxviews
"
],
initialize
()
{
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api
.
settings
.
extend
({
'
autocomplete_add_contact
'
:
true
,
'
allow_chat_pending_contacts
'
:
true
,
'
allow_contact_removal
'
:
true
,
'
hide_offline_users
'
:
false
,
'
roster_groups
'
:
true
,
'
xhr_user_search_url
'
:
null
,
});
api
.
promises
.
add
(
'
rosterViewInitialized
'
);
_converse
.
RosterFilter
=
RosterFilter
;
_converse
.
RosterFilterView
=
RosterFilterView
;
_converse
.
RosterContactView
=
RosterContactView
;
_converse
.
RosterGroupView
=
RosterGroupView
;
_converse
.
RosterView
=
RosterView
;
/* -------- Event Handlers ----------- */
api
.
listen
.
on
(
'
chatBoxesInitialized
'
,
()
=>
{
function
highlightRosterItem
(
chatbox
)
{
const
contact
=
_converse
.
roster
&&
_converse
.
roster
.
findWhere
({
'
jid
'
:
chatbox
.
get
(
'
jid
'
)});
if
(
contact
!==
undefined
)
{
contact
.
trigger
(
'
highlight
'
);
}
}
_converse
.
chatboxes
.
on
(
'
destroy
'
,
chatbox
=>
highlightRosterItem
(
chatbox
));
_converse
.
chatboxes
.
on
(
'
change:hidden
'
,
chatbox
=>
highlightRosterItem
(
chatbox
));
});
api
.
listen
.
on
(
'
controlBoxInitialized
'
,
(
view
)
=>
{
function
insertRoster
()
{
if
(
!
view
.
model
.
get
(
'
connected
'
)
||
api
.
settings
.
get
(
"
authentication
"
)
===
_converse
.
ANONYMOUS
)
{
return
;
}
/* Place the rosterview inside the "Contacts" panel. */
api
.
waitUntil
(
'
rosterViewInitialized
'
)
.
then
(()
=>
view
.
controlbox_pane
.
el
.
insertAdjacentElement
(
'
beforeEnd
'
,
_converse
.
rosterview
.
el
))
.
catch
(
e
=>
log
.
fatal
(
e
));
}
insertRoster
();
view
.
model
.
on
(
'
change:connected
'
,
insertRoster
);
});
function
initRosterView
()
{
/* Create an instance of RosterView once the RosterGroups
* collection has been created (in @converse/headless/core.js)
*/
if
(
api
.
settings
.
get
(
"
authentication
"
)
===
_converse
.
ANONYMOUS
)
{
return
;
}
_converse
.
rosterview
=
new
_converse
.
RosterView
({
'
model
'
:
_converse
.
rostergroups
});
_converse
.
rosterview
.
render
();
/**
* Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized
* @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
*/
api
.
trigger
(
'
rosterViewInitialized
'
);
}
api
.
listen
.
on
(
'
rosterInitialized
'
,
initRosterView
);
api
.
listen
.
on
(
'
rosterReadyAfterReconnection
'
,
initRosterView
);
api
.
listen
.
on
(
'
afterTearDown
'
,
()
=>
{
if
(
converse
.
rosterview
)
{
converse
.
rosterview
.
model
.
off
().
reset
();
converse
.
rosterview
.
each
(
groupview
=>
groupview
.
removeAll
().
remove
());
converse
.
rosterview
.
removeAll
().
remove
();
delete
converse
.
rosterview
;
}
});
}
});
src/plugins/rosterview/rosterview.js
0 → 100644
View file @
794a7096
import
RosterGroupView
from
'
./groupview.js
'
;
import
log
from
"
@converse/headless/log
"
;
import
tpl_roster
from
"
./templates/roster.html
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
OrderedListView
}
from
"
@converse/skeletor/src/overview
"
;
import
{
__
}
from
'
i18n
'
;
import
{
_converse
,
api
,
converse
}
from
"
@converse/headless/core
"
;
import
{
debounce
,
has
}
from
"
lodash-es
"
;
const
u
=
converse
.
env
.
utils
;
/**
* @class
* @namespace _converse.RosterView
* @memberOf _converse
*/
const
RosterView
=
OrderedListView
.
extend
({
tagName
:
'
div
'
,
id
:
'
converse-roster
'
,
className
:
'
controlbox-section
'
,
ItemView
:
RosterGroupView
,
listItems
:
'
model
'
,
listSelector
:
'
.roster-contacts
'
,
sortEvent
:
null
,
// Groups are immutable, so they don't get re-sorted
subviewIndex
:
'
name
'
,
sortImmediatelyOnAdd
:
true
,
events
:
{
'
click a.controlbox-heading__btn.add-contact
'
:
'
showAddContactModal
'
,
'
click a.controlbox-heading__btn.sync-contacts
'
:
'
syncContacts
'
},
initialize
()
{
OrderedListView
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
this
.
listenTo
(
_converse
.
roster
,
"
add
"
,
this
.
onContactAdded
);
this
.
listenTo
(
_converse
.
roster
,
'
change:groups
'
,
this
.
onContactAdded
);
this
.
listenTo
(
_converse
.
roster
,
'
change
'
,
this
.
onContactChange
);
this
.
listenTo
(
_converse
.
roster
,
"
destroy
"
,
this
.
update
);
this
.
listenTo
(
_converse
.
roster
,
"
remove
"
,
this
.
update
);
_converse
.
presences
.
on
(
'
change:show
'
,
()
=>
{
this
.
update
();
this
.
updateFilter
();
});
this
.
listenTo
(
this
.
model
,
"
reset
"
,
this
.
reset
);
// This event gets triggered once *all* contacts (i.e. not
// just this group's) have been fetched from browser
// storage or the XMPP server and once they've been
// assigned to their various groups.
api
.
listen
.
on
(
'
rosterGroupsFetched
'
,
this
.
sortAndPositionAllItems
.
bind
(
this
));
api
.
listen
.
on
(
'
rosterContactsFetched
'
,
()
=>
{
_converse
.
roster
.
each
(
contact
=>
this
.
addRosterContact
(
contact
,
{
'
silent
'
:
true
}));
this
.
update
();
this
.
updateFilter
();
this
.
trigger
(
'
rosterContactsFetchedAndProcessed
'
);
});
this
.
createRosterFilter
();
},
render
()
{
this
.
el
.
innerHTML
=
tpl_roster
({
'
allow_contact_requests
'
:
_converse
.
allow_contact_requests
,
'
heading_contacts
'
:
__
(
'
Contacts
'
),
'
title_add_contact
'
:
__
(
'
Add a contact
'
),
'
title_sync_contacts
'
:
__
(
'
Re-sync your contacts
'
)
});
const
form
=
this
.
el
.
querySelector
(
'
.roster-filter-form
'
);
this
.
el
.
replaceChild
(
this
.
filter_view
.
render
().
el
,
form
);
this
.
roster_el
=
this
.
el
.
querySelector
(
'
.roster-contacts
'
);
return
this
;
},
showAddContactModal
(
ev
)
{
api
.
modal
.
show
(
_converse
.
AddContactModal
,
{
'
model
'
:
new
Model
()},
ev
);
},
createRosterFilter
()
{
// Create a model on which we can store filter properties
const
model
=
new
_converse
.
RosterFilter
();
model
.
id
=
`_converse.rosterfilter-
${
_converse
.
bare_jid
}
`
;
model
.
browserStorage
=
_converse
.
createStore
(
model
.
id
);
this
.
filter_view
=
new
_converse
.
RosterFilterView
({
model
});
this
.
listenTo
(
this
.
filter_view
.
model
,
'
change
'
,
this
.
updateFilter
);
this
.
filter_view
.
model
.
fetch
();
},
/**
* Called whenever the filter settings have been changed or
* when contacts have been added, removed or changed.
*
* Debounced for 100ms so that it doesn't get called for every
* contact fetched from browser storage.
*/
updateFilter
:
debounce
(
function
()
{
const
type
=
this
.
filter_view
.
model
.
get
(
'
filter_type
'
);
if
(
type
===
'
state
'
)
{
this
.
filter
(
this
.
filter_view
.
model
.
get
(
'
chat_state
'
),
type
);
}
else
{
this
.
filter
(
this
.
filter_view
.
model
.
get
(
'
filter_text
'
),
type
);
}
},
100
),
update
()
{
if
(
!
u
.
isVisible
(
this
.
roster_el
))
{
u
.
showElement
(
this
.
roster_el
);
}
this
.
filter_view
.
render
();
return
this
;
},
filter
(
query
,
type
)
{
const
views
=
Object
.
values
(
this
.
getAll
());
// First ensure the filter is restored to its original state
views
.
forEach
(
v
=>
(
v
.
model
.
contacts
.
length
>
0
)
&&
v
.
show
().
filter
(
''
));
// Now we can filter
query
=
query
.
toLowerCase
();
if
(
type
===
'
groups
'
)
{
views
.
forEach
(
view
=>
{
if
(
!
view
.
model
.
get
(
'
name
'
).
toLowerCase
().
includes
(
query
))
{
u
.
slideIn
(
view
.
el
);
}
else
if
(
view
.
model
.
contacts
.
length
>
0
)
{
u
.
slideOut
(
view
.
el
);
}
});
}
else
{
views
.
forEach
(
v
=>
v
.
filter
(
query
,
type
));
}
},
async
syncContacts
(
ev
)
{
ev
.
preventDefault
();
u
.
addClass
(
'
fa-spin
'
,
ev
.
target
);
_converse
.
roster
.
data
.
save
(
'
version
'
,
null
);
await
_converse
.
roster
.
fetchFromServer
();
api
.
user
.
presence
.
send
();
u
.
removeClass
(
'
fa-spin
'
,
ev
.
target
);
},
reset
()
{
this
.
removeAll
();
this
.
render
().
update
();
return
this
;
},
onContactAdded
(
contact
)
{
this
.
addRosterContact
(
contact
)
this
.
update
();
this
.
updateFilter
();
},
onContactChange
(
contact
)
{
this
.
update
();
if
(
has
(
contact
.
changed
,
'
subscription
'
))
{
if
(
contact
.
changed
.
subscription
===
'
from
'
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_PENDING_CONTACTS
);
}
else
if
([
'
both
'
,
'
to
'
].
includes
(
contact
.
get
(
'
subscription
'
)))
{
this
.
addExistingContact
(
contact
);
}
}
if
(
has
(
contact
.
changed
,
'
num_unread
'
)
&&
contact
.
get
(
'
num_unread
'
))
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_UNREAD
);
}
if
(
has
(
contact
.
changed
,
'
ask
'
)
&&
contact
.
changed
.
ask
===
'
subscribe
'
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_PENDING_CONTACTS
);
}
if
(
has
(
contact
.
changed
,
'
subscription
'
)
&&
contact
.
changed
.
requesting
===
'
true
'
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_REQUESTING_CONTACTS
);
}
this
.
updateFilter
();
},
/**
* Returns the group as specified by name.
* Creates the group if it doesn't exist.
* @method _converse.RosterView#getGroup
* @private
* @param {string} name
*/
getGroup
(
name
)
{
const
view
=
this
.
get
(
name
);
if
(
view
)
{
return
view
.
model
;
}
return
this
.
model
.
create
({
name
});
},
addContactToGroup
(
contact
,
name
,
options
)
{
this
.
getGroup
(
name
).
contacts
.
add
(
contact
,
options
);
this
.
sortAndPositionAllItems
();
},
addExistingContact
(
contact
,
options
)
{
let
groups
;
if
(
api
.
settings
.
get
(
'
roster_groups
'
))
{
groups
=
contact
.
get
(
'
groups
'
);
groups
=
(
groups
.
length
===
0
)
?
[
_converse
.
HEADER_UNGROUPED
]
:
groups
;
}
else
{
groups
=
[
_converse
.
HEADER_CURRENT_CONTACTS
];
}
if
(
contact
.
get
(
'
num_unread
'
))
{
groups
.
push
(
_converse
.
HEADER_UNREAD
);
}
groups
.
forEach
(
g
=>
this
.
addContactToGroup
(
contact
,
g
,
options
));
},
isSelf
(
jid
)
{
return
u
.
isSameBareJID
(
jid
,
_converse
.
connection
.
jid
);
},
addRosterContact
(
contact
,
options
)
{
const
jid
=
contact
.
get
(
'
jid
'
);
if
(
contact
.
get
(
'
subscription
'
)
===
'
both
'
||
contact
.
get
(
'
subscription
'
)
===
'
to
'
||
this
.
isSelf
(
jid
))
{
this
.
addExistingContact
(
contact
,
options
);
}
else
{
if
(
!
_converse
.
allow_contact_requests
)
{
log
.
debug
(
`Not adding requesting or pending contact
${
jid
}
`
+
`because allow_contact_requests is false`
);
return
;
}
if
((
contact
.
get
(
'
ask
'
)
===
'
subscribe
'
)
||
(
contact
.
get
(
'
subscription
'
)
===
'
from
'
))
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_PENDING_CONTACTS
,
options
);
}
else
if
(
contact
.
get
(
'
requesting
'
)
===
true
)
{
this
.
addContactToGroup
(
contact
,
_converse
.
HEADER_REQUESTING_CONTACTS
,
options
);
}
}
return
this
;
}
});
export
default
RosterView
;
src/templates/group_header.html
→
src/
plugins/rosterview/
templates/group_header.html
View file @
794a7096
File moved
src/templates/pending_contact.html
→
src/
plugins/rosterview/
templates/pending_contact.html
View file @
794a7096
File moved
src/templates/requesting_contact.html
→
src/
plugins/rosterview/
templates/requesting_contact.html
View file @
794a7096
File moved
src/templates/roster.html
→
src/
plugins/rosterview/
templates/roster.html
View file @
794a7096
File moved
src/templates/roster_filter.js
→
src/
plugins/rosterview/
templates/roster_filter.js
View file @
794a7096
File moved
src/templates/roster_item.html
→
src/
plugins/rosterview/
templates/roster_item.html
View file @
794a7096
File moved
src/shared/avatar.js
View file @
794a7096
import
tpl_avatar
from
'
templates/avatar.js
'
;
import
{
View
}
from
'
@converse/skeletor/src/view
'
;
import
{
converse
}
from
'
@converse/headless/core
'
;
const
u
=
converse
.
env
.
utils
;
const
AvatarMixin
=
{
const
ViewWithAvatar
=
View
.
extend
(
{
renderAvatar
(
el
)
{
el
=
el
||
this
.
el
;
const
avatar_el
=
el
.
querySelector
(
'
canvas.avatar, svg.avatar
'
);
...
...
@@ -21,6 +22,6 @@ const AvatarMixin = {
avatar_el
.
outerHTML
=
u
.
getElementFromTemplateResult
(
tpl_avatar
(
data
)).
outerHTML
;
}
}
};
}
)
;
export
default
AvatarMixin
;
export
default
ViewWithAvatar
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment