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
e3ebde97
Commit
e3ebde97
authored
Dec 04, 2020
by
JC Brand
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move converse-chat plugin into folder
parent
01e03fc6
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
1577 additions
and
1518 deletions
+1577
-1518
package-lock.json
package-lock.json
+10
-10
spec/corrections.js
spec/corrections.js
+1
-1
src/headless/headless.js
src/headless/headless.js
+1
-1
src/headless/plugins/chat.js
src/headless/plugins/chat.js
+0
-1504
src/headless/plugins/chat/api.js
src/headless/plugins/chat/api.js
+144
-0
src/headless/plugins/chat/index.js
src/headless/plugins/chat/index.js
+203
-0
src/headless/plugins/chat/message.js
src/headless/plugins/chat/message.js
+239
-0
src/headless/plugins/chat/model-with-contact.js
src/headless/plugins/chat/model-with-contact.js
+23
-0
src/headless/plugins/chat/model.js
src/headless/plugins/chat/model.js
+951
-0
src/headless/plugins/muc.js
src/headless/plugins/muc.js
+5
-2
No files found.
package-lock.json
View file @
e3ebde97
...
...
@@ -4482,9 +4482,9 @@
}
},
"@octokit/openapi-types"
:
{
"version"
:
"
1.2.2
"
,
"resolved"
:
"https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-
1.2.2
.tgz"
,
"integrity"
:
"sha512-
vrKDLd/Rq4IE16oT+jJkDBx0r29NFkdkU8GwqVSP4RajsAvP23CMGtFhVK0pedUhAiMvG1bGnFcTC/xCKaKgm
w=="
,
"version"
:
"
2.0.0
"
,
"resolved"
:
"https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-
2.0.0
.tgz"
,
"integrity"
:
"sha512-
J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnP
w=="
,
"dev"
:
true
},
"@octokit/plugin-enterprise-rest"
:
{
...
...
@@ -4628,12 +4628,12 @@
}
},
"@octokit/types"
:
{
"version"
:
"6.
0.3
"
,
"resolved"
:
"https://registry.npmjs.org/@octokit/types/-/types-6.
0.3
.tgz"
,
"integrity"
:
"sha512-
6y0Emzp+uPpdC5QLzUY1YRklvqiZBMTOz2ByhXdmTFlc3lNv8Mi28dX1U1b4scNtFMUa3tkpjofNFJ5NqMJaZw
=="
,
"version"
:
"6.
1.0
"
,
"resolved"
:
"https://registry.npmjs.org/@octokit/types/-/types-6.
1.0
.tgz"
,
"integrity"
:
"sha512-
bMWBmg77MQTiRkOVyf50qK3QECWOEy43rLy/6fTWZ4HEwAhNfqzMcjiBDZAowkILwTrFvzE1CpP6gD0MuPHS+A
=="
,
"dev"
:
true
,
"requires"
:
{
"@octokit/openapi-types"
:
"^
1.2
.0"
,
"@octokit/openapi-types"
:
"^
2.0
.0"
,
"@types/node"
:
">= 8"
}
},
...
...
@@ -22471,9 +22471,9 @@
},
"dependencies"
:
{
"ws"
:
{
"version"
:
"7.4.
0
"
,
"resolved"
:
"https://registry.npmjs.org/ws/-/ws-7.4.
0
.tgz"
,
"integrity"
:
"sha512-
kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4N
Q=="
,
"version"
:
"7.4.
1
"
,
"resolved"
:
"https://registry.npmjs.org/ws/-/ws-7.4.
1
.tgz"
,
"integrity"
:
"sha512-
pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGF
Q=="
,
"optional"
:
true
}
}
...
...
spec/corrections.js
View file @
e3ebde97
...
...
@@ -80,7 +80,7 @@ describe("A Chat Message", function () {
await
u
.
waitUntil
(()
=>
(
u
.
hasClass
(
'
correcting
'
,
view
.
el
.
querySelector
(
'
.chat-msg
'
))
===
false
),
500
);
// Test that pressing the down arrow cancels message correction
expect
(
textarea
.
value
).
toBe
(
''
);
await
u
.
waitUntil
(()
=>
textarea
.
value
===
''
)
view
.
onKeyDown
({
target
:
textarea
,
keyCode
:
38
// Up arrow
...
...
src/headless/headless.js
View file @
e3ebde97
...
...
@@ -7,7 +7,7 @@ import "./plugins/bookmarks.js"; // XEP-0199 XMPP Ping
import
"
./plugins/bosh.js
"
;
// XEP-0206 BOSH
import
"
./plugins/caps.js
"
;
// XEP-0115 Entity Capabilities
import
"
./plugins/carbons.js
"
;
// XEP-0280 Message Carbons
import
"
./plugins/chat.js
"
;
// RFC-6121 Instant messaging
import
"
./plugins/chat
/index
.js
"
;
// RFC-6121 Instant messaging
import
"
./plugins/chatboxes.js
"
;
import
"
./plugins/disco.js
"
;
// XEP-0030 Service discovery
import
"
./plugins/headlines.js
"
;
// Support for headline messages
...
...
src/headless/plugins/chat.js
deleted
100644 → 0
View file @
01e03fc6
/**
* @module converse-chat
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import
filesize
from
"
filesize
"
;
import
log
from
"
../log.js
"
;
import
st
from
"
../utils/stanza
"
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
_converse
,
api
,
converse
}
from
"
../core.js
"
;
import
{
find
,
isMatch
,
isObject
,
pick
}
from
"
lodash-es
"
;
const
{
$msg
,
Strophe
,
sizzle
,
utils
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
converse
.
plugins
.
add
(
'
converse-chat
'
,
{
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies
:
[
"
converse-chatboxes
"
,
"
converse-disco
"
],
initialize
()
{
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const
{
__
}
=
_converse
;
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
api
.
settings
.
extend
({
'
allow_message_corrections
'
:
'
all
'
,
'
allow_message_retraction
'
:
'
all
'
,
'
allow_message_styling
'
:
true
,
'
auto_join_private_chats
'
:
[],
'
clear_messages_on_reconnection
'
:
false
,
'
filter_by_resource
'
:
false
,
'
send_chat_state_notifications
'
:
true
});
const
ModelWithContact
=
Model
.
extend
({
initialize
()
{
this
.
rosterContactAdded
=
u
.
getResolveablePromise
();
},
async
setRosterContact
(
jid
)
{
const
contact
=
await
api
.
contacts
.
get
(
jid
);
if
(
contact
)
{
this
.
contact
=
contact
;
this
.
set
(
'
nickname
'
,
contact
.
get
(
'
nickname
'
));
this
.
rosterContactAdded
.
resolve
();
}
}
});
/**
* Represents a non-MUC message. These can be either `chat` messages or
* `headline` messages.
* @class
* @namespace _converse.Message
* @memberOf _converse
* @example const msg = new _converse.Message({'message': 'hello world!'});
*/
_converse
.
Message
=
ModelWithContact
.
extend
({
defaults
()
{
return
{
'
msgid
'
:
u
.
getUniqueId
(),
'
time
'
:
(
new
Date
()).
toISOString
(),
'
is_ephemeral
'
:
false
};
},
async
initialize
()
{
if
(
!
this
.
checkValidity
())
{
return
;
}
this
.
initialized
=
u
.
getResolveablePromise
();
if
(
this
.
get
(
'
type
'
)
===
'
chat
'
)
{
ModelWithContact
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
this
.
setRosterContact
(
Strophe
.
getBareJidFromJid
(
this
.
get
(
'
from
'
)));
}
if
(
this
.
get
(
'
file
'
))
{
this
.
on
(
'
change:put
'
,
this
.
uploadFile
,
this
);
}
this
.
setTimerForEphemeralMessage
();
/**
* Triggered once a {@link _converse.Message} has been created and initialized.
* @event _converse#messageInitialized
* @type { _converse.Message}
* @example _converse.api.listen.on('messageInitialized', model => { ... });
*/
await
api
.
trigger
(
'
messageInitialized
'
,
this
,
{
'
Synchronous
'
:
true
});
this
.
initialized
.
resolve
();
},
/**
* Sets an auto-destruct timer for this message, if it's is_ephemeral.
* @private
* @method _converse.Message#setTimerForEphemeralMessage
* @returns { Boolean } - Indicates whether the message is
* ephemeral or not, and therefore whether the timer was set or not.
*/
setTimerForEphemeralMessage
()
{
const
setTimer
=
()
=>
{
this
.
ephemeral_timer
=
window
.
setTimeout
(
this
.
safeDestroy
.
bind
(
this
),
10000
);
}
if
(
this
.
isEphemeral
())
{
setTimer
();
return
true
;
}
else
{
this
.
on
(
'
change:is_ephemeral
'
,
()
=>
this
.
isEphemeral
()
?
setTimer
()
:
clearTimeout
(
this
.
ephemeral_timer
)
);
return
false
;
}
},
checkValidity
()
{
if
(
Object
.
keys
(
this
.
attributes
).
length
===
3
)
{
// XXX: This is an empty message with only the 3 default values.
// This seems to happen when saving a newly created message
// fails for some reason.
// TODO: This is likely fixable by setting `wait` when
// creating messages. See the wait-for-messages branch.
this
.
validationError
=
"
Empty message
"
;
this
.
safeDestroy
();
return
false
;
}
return
true
;
},
/**
* Determines whether this messsage may be retracted by the current user.
* @private
* @method _converse.Messages#mayBeRetracted
* @returns { Boolean }
*/
mayBeRetracted
()
{
const
is_own_message
=
this
.
get
(
'
sender
'
)
===
'
me
'
;
return
is_own_message
&&
[
'
all
'
,
'
own
'
].
includes
(
api
.
settings
.
get
(
'
allow_message_retraction
'
));
},
safeDestroy
()
{
try
{
this
.
destroy
()
}
catch
(
e
)
{
log
.
error
(
e
);
}
},
isEphemeral
()
{
return
this
.
get
(
'
is_ephemeral
'
);
},
getDisplayName
()
{
if
(
this
.
get
(
'
type
'
)
===
'
groupchat
'
)
{
return
this
.
get
(
'
nick
'
);
}
else
if
(
this
.
contact
)
{
return
this
.
contact
.
getDisplayName
();
}
else
if
(
this
.
vcard
)
{
return
this
.
vcard
.
getDisplayName
();
}
else
{
return
this
.
get
(
'
from
'
);
}
},
getMessageText
()
{
if
(
this
.
get
(
'
is_encrypted
'
))
{
return
this
.
get
(
'
plaintext
'
)
||
this
.
get
(
'
body
'
)
||
__
(
'
Undecryptable OMEMO message
'
);
}
return
this
.
get
(
'
message
'
);
},
isMeCommand
()
{
const
text
=
this
.
getMessageText
();
if
(
!
text
)
{
return
false
;
}
return
text
.
startsWith
(
'
/me
'
);
},
sendSlotRequestStanza
()
{
/* Send out an IQ stanza to request a file upload slot.
*
* https://xmpp.org/extensions/xep-0363.html#request
*/
if
(
!
this
.
file
)
{
return
Promise
.
reject
(
new
Error
(
"
file is undefined
"
));
}
const
iq
=
converse
.
env
.
$iq
({
'
from
'
:
_converse
.
jid
,
'
to
'
:
this
.
get
(
'
slot_request_url
'
),
'
type
'
:
'
get
'
}).
c
(
'
request
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HTTPUPLOAD
,
'
filename
'
:
this
.
file
.
name
,
'
size
'
:
this
.
file
.
size
,
'
content-type
'
:
this
.
file
.
type
})
return
api
.
sendIQ
(
iq
);
},
async
getRequestSlotURL
()
{
let
stanza
;
try
{
stanza
=
await
this
.
sendSlotRequestStanza
();
}
catch
(
e
)
{
log
.
error
(
e
);
return
this
.
save
({
'
type
'
:
'
error
'
,
'
message
'
:
__
(
"
Sorry, could not determine upload URL.
"
),
'
is_ephemeral
'
:
true
});
}
const
slot
=
stanza
.
querySelector
(
'
slot
'
);
if
(
slot
)
{
this
.
save
({
'
get
'
:
slot
.
querySelector
(
'
get
'
).
getAttribute
(
'
url
'
),
'
put
'
:
slot
.
querySelector
(
'
put
'
).
getAttribute
(
'
url
'
),
});
}
else
{
return
this
.
save
({
'
type
'
:
'
error
'
,
'
message
'
:
__
(
"
Sorry, could not determine file upload URL.
"
),
'
is_ephemeral
'
:
true
});
}
},
uploadFile
()
{
const
xhr
=
new
XMLHttpRequest
();
xhr
.
onreadystatechange
=
()
=>
{
if
(
xhr
.
readyState
===
XMLHttpRequest
.
DONE
)
{
log
.
info
(
"
Status:
"
+
xhr
.
status
);
if
(
xhr
.
status
===
200
||
xhr
.
status
===
201
)
{
this
.
save
({
'
upload
'
:
_converse
.
SUCCESS
,
'
oob_url
'
:
this
.
get
(
'
get
'
),
'
message
'
:
this
.
get
(
'
get
'
)
});
}
else
{
xhr
.
onerror
();
}
}
};
xhr
.
upload
.
addEventListener
(
"
progress
"
,
(
evt
)
=>
{
if
(
evt
.
lengthComputable
)
{
this
.
set
(
'
progress
'
,
evt
.
loaded
/
evt
.
total
);
}
},
false
);
xhr
.
onerror
=
()
=>
{
let
message
;
if
(
xhr
.
responseText
)
{
message
=
__
(
'
Sorry, could not succesfully upload your file. Your server’s response: "%1$s"
'
,
xhr
.
responseText
)
}
else
{
message
=
__
(
'
Sorry, could not succesfully upload your file.
'
);
}
this
.
save
({
'
type
'
:
'
error
'
,
'
upload
'
:
_converse
.
FAILURE
,
'
message
'
:
message
,
'
is_ephemeral
'
:
true
});
};
xhr
.
open
(
'
PUT
'
,
this
.
get
(
'
put
'
),
true
);
xhr
.
setRequestHeader
(
"
Content-type
"
,
this
.
file
.
type
);
xhr
.
send
(
this
.
file
);
}
});
_converse
.
Messages
=
Collection
.
extend
({
model
:
_converse
.
Message
,
comparator
:
'
time
'
});
/**
* Represents an open/ongoing chat conversation.
*
* @class
* @namespace _converse.ChatBox
* @memberOf _converse
*/
_converse
.
ChatBox
=
ModelWithContact
.
extend
({
messagesCollection
:
_converse
.
Messages
,
defaults
()
{
return
{
'
bookmarked
'
:
false
,
'
chat_state
'
:
undefined
,
'
hidden
'
:
_converse
.
isUniView
()
&&
!
api
.
settings
.
get
(
'
singleton
'
),
'
message_type
'
:
'
chat
'
,
'
nickname
'
:
undefined
,
'
num_unread
'
:
0
,
'
time_sent
'
:
(
new
Date
(
0
)).
toISOString
(),
'
time_opened
'
:
this
.
get
(
'
time_opened
'
)
||
(
new
Date
()).
getTime
(),
'
type
'
:
_converse
.
PRIVATE_CHAT_TYPE
,
'
url
'
:
''
}
},
async
initialize
()
{
this
.
initialized
=
u
.
getResolveablePromise
();
ModelWithContact
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
const
jid
=
this
.
get
(
'
jid
'
);
if
(
!
jid
)
{
// XXX: The `validate` method will prevent this model
// from being persisted if there's no jid, but that gets
// called after model instantiation, so we have to deal
// with invalid models here also.
// This happens when the controlbox is in browser storage,
// but we're in embedded mode.
return
;
}
this
.
set
({
'
box_id
'
:
`box-
${
jid
}
`
});
this
.
initNotifications
();
this
.
initMessages
();
if
(
this
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
)
{
this
.
presence
=
_converse
.
presences
.
findWhere
({
'
jid
'
:
jid
})
||
_converse
.
presences
.
create
({
'
jid
'
:
jid
});
await
this
.
setRosterContact
(
jid
);
}
this
.
on
(
'
change:chat_state
'
,
this
.
sendChatState
,
this
);
await
this
.
fetchMessages
();
/**
* Triggered once a {@link _converse.ChatBox} has been created and initialized.
* @event _converse#chatBoxInitialized
* @type { _converse.ChatBox}
* @example _converse.api.listen.on('chatBoxInitialized', model => { ... });
*/
await
api
.
trigger
(
'
chatBoxInitialized
'
,
this
,
{
'
Synchronous
'
:
true
});
this
.
initialized
.
resolve
();
},
getMessagesCacheKey
()
{
return
`converse.messages-
${
this
.
get
(
'
jid
'
)}
-
${
_converse
.
bare_jid
}
`
;
},
initMessages
()
{
this
.
messages
=
new
this
.
messagesCollection
();
this
.
messages
.
fetched
=
u
.
getResolveablePromise
();
this
.
messages
.
fetched
.
then
(()
=>
{
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBoxView | _converse.ChatRoomView}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
api
.
trigger
(
'
afterMessagesFetched
'
,
this
);
});
this
.
messages
.
chatbox
=
this
;
this
.
messages
.
browserStorage
=
_converse
.
createStore
(
this
.
getMessagesCacheKey
());
this
.
listenTo
(
this
.
messages
,
'
change:upload
'
,
message
=>
{
if
(
message
.
get
(
'
upload
'
)
===
_converse
.
SUCCESS
)
{
api
.
send
(
this
.
createMessageStanza
(
message
));
}
});
},
initNotifications
()
{
this
.
notifications
=
new
Model
();
},
afterMessagesFetched
()
{
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
api
.
trigger
(
'
afterMessagesFetched
'
,
this
);
},
fetchMessages
()
{
if
(
this
.
messages
.
fetched_flag
)
{
log
.
info
(
`Not re-fetching messages for
${
this
.
get
(
'
jid
'
)}
`
);
return
;
}
this
.
messages
.
fetched_flag
=
true
;
const
resolve
=
this
.
messages
.
fetched
.
resolve
;
this
.
messages
.
fetch
({
'
add
'
:
true
,
'
success
'
:
()
=>
{
this
.
afterMessagesFetched
();
resolve
()
},
'
error
'
:
()
=>
{
this
.
afterMessagesFetched
();
resolve
()
}
});
return
this
.
messages
.
fetched
;
},
async
handleErrorMessageStanza
(
stanza
)
{
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
if
(
!
await
this
.
shouldShowErrorMessage
(
attrs
))
{
return
;
}
const
message
=
this
.
getMessageReferencedByError
(
attrs
);
if
(
message
)
{
const
new_attrs
=
{
'
error
'
:
attrs
.
error
,
'
error_condition
'
:
attrs
.
error_condition
,
'
error_text
'
:
attrs
.
error_text
,
'
error_type
'
:
attrs
.
error_type
,
'
editable
'
:
false
,
};
if
(
attrs
.
msgid
===
message
.
get
(
'
retraction_id
'
))
{
// The error message refers to a retraction
new_attrs
.
retraction_id
=
undefined
;
if
(
!
attrs
.
error
)
{
if
(
attrs
.
error_condition
===
'
forbidden
'
)
{
new_attrs
.
error
=
__
(
"
You're not allowed to retract your message.
"
);
}
else
{
new_attrs
.
error
=
__
(
'
Sorry, an error occurred while trying to retract your message.
'
);
}
}
}
else
if
(
!
attrs
.
error
)
{
if
(
attrs
.
error_condition
===
'
forbidden
'
)
{
new_attrs
.
error
=
__
(
"
You're not allowed to send a message.
"
);
}
else
{
new_attrs
.
error
=
__
(
'
Sorry, an error occurred while trying to send your message.
'
);
}
}
message
.
save
(
new_attrs
);
}
else
{
this
.
createMessage
(
attrs
);
}
},
/**
* Queue an incoming `chat` message stanza for processing.
* @async
* @private
* @method _converse.ChatRoom#queueMessage
* @param { Promise<MessageAttributes> } attrs - A promise which resolves to the message attributes
*/
queueMessage
(
attrs
)
{
this
.
msg_chain
=
(
this
.
msg_chain
||
this
.
messages
.
fetched
)
.
then
(()
=>
this
.
onMessage
(
attrs
))
.
catch
(
e
=>
log
.
error
(
e
));
return
this
.
msg_chain
;
},
/**
* @async
* @private
* @method _converse.ChatRoom#onMessage
* @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes.
*/
async
onMessage
(
attrs
)
{
attrs
=
await
attrs
;
if
(
u
.
isErrorObject
(
attrs
))
{
attrs
.
stanza
&&
log
.
error
(
attrs
.
stanza
);
return
log
.
error
(
attrs
.
message
);
}
const
message
=
this
.
getDuplicateMessage
(
attrs
);
if
(
message
)
{
this
.
updateMessage
(
message
,
attrs
);
}
else
if
(
!
this
.
handleReceipt
(
attrs
)
&&
!
this
.
handleChatMarker
(
attrs
)
&&
!
(
await
this
.
handleRetraction
(
attrs
))
)
{
this
.
setEditable
(
attrs
,
attrs
.
time
);
if
(
attrs
[
'
chat_state
'
]
&&
attrs
.
sender
===
'
them
'
)
{
this
.
notifications
.
set
(
'
chat_state
'
,
attrs
.
chat_state
);
}
if
(
u
.
shouldCreateMessage
(
attrs
))
{
const
msg
=
this
.
handleCorrection
(
attrs
)
||
await
this
.
createMessage
(
attrs
);
this
.
notifications
.
set
({
'
chat_state
'
:
null
});
this
.
handleUnreadMessage
(
msg
);
}
}
},
async
clearMessages
()
{
try
{
await
this
.
messages
.
clearStore
();
}
catch
(
e
)
{
this
.
messages
.
trigger
(
'
reset
'
);
log
.
error
(
e
);
}
finally
{
delete
this
.
msg_chain
;
delete
this
.
messages
.
fetched_flag
;
this
.
messages
.
fetched
=
u
.
getResolveablePromise
();
}
},
async
close
()
{
try
{
await
new
Promise
((
success
,
reject
)
=>
{
return
this
.
destroy
({
success
,
'
error
'
:
(
m
,
e
)
=>
reject
(
e
)})
});
}
catch
(
e
)
{
log
.
error
(
e
);
}
finally
{
if
(
api
.
settings
.
get
(
'
clear_messages_on_reconnection
'
))
{
await
this
.
clearMessages
();
}
}
},
announceReconnection
()
{
/**
* Triggered whenever a `_converse.ChatBox` instance has reconnected after an outage
* @event _converse#onChatReconnected
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('onChatReconnected', chatbox => { ... });
*/
api
.
trigger
(
'
chatReconnected
'
,
this
);
},
async
onReconnection
()
{
if
(
api
.
settings
.
get
(
'
clear_messages_on_reconnection
'
))
{
await
this
.
clearMessages
();
}
this
.
announceReconnection
();
},
validate
(
attrs
)
{
if
(
!
attrs
.
jid
)
{
return
'
Ignored ChatBox without JID
'
;
}
const
room_jids
=
_converse
.
auto_join_rooms
.
map
(
s
=>
isObject
(
s
)
?
s
.
jid
:
s
);
const
auto_join
=
api
.
settings
.
get
(
'
auto_join_private_chats
'
).
concat
(
room_jids
);
if
(
api
.
settings
.
get
(
"
singleton
"
)
&&
!
auto_join
.
includes
(
attrs
.
jid
)
&&
!
api
.
settings
.
get
(
'
auto_join_on_invite
'
))
{
const
msg
=
`
${
attrs
.
jid
}
is not allowed because singleton is true and it's not being auto_joined`
;
log
.
warn
(
msg
);
return
msg
;
}
},
getDisplayName
()
{
if
(
this
.
contact
)
{
return
this
.
contact
.
getDisplayName
();
}
else
if
(
this
.
vcard
)
{
return
this
.
vcard
.
getDisplayName
();
}
else
{
return
this
.
get
(
'
jid
'
);
}
},
async
createMessageFromError
(
error
)
{
if
(
error
instanceof
_converse
.
TimeoutError
)
{
const
msg
=
await
this
.
createMessage
({
'
type
'
:
'
error
'
,
'
message
'
:
error
.
message
,
'
retry_event_id
'
:
error
.
retry_event_id
});
msg
.
error
=
error
;
}
},
getOldestMessage
()
{
for
(
let
i
=
0
;
i
<
this
.
messages
.
length
;
i
++
)
{
const
message
=
this
.
messages
.
at
(
i
);
if
(
message
.
get
(
'
type
'
)
===
this
.
get
(
'
message_type
'
))
{
return
message
;
}
}
},
getMostRecentMessage
()
{
for
(
let
i
=
this
.
messages
.
length
-
1
;
i
>=
0
;
i
--
)
{
const
message
=
this
.
messages
.
at
(
i
);
if
(
message
.
get
(
'
type
'
)
===
this
.
get
(
'
message_type
'
))
{
return
message
;
}
}
},
getUpdatedMessageAttributes
(
message
,
attrs
)
{
// Filter the attrs object, restricting it to only the `is_archived` key.
return
(({
is_archived
})
=>
({
is_archived
}))(
attrs
)
},
updateMessage
(
message
,
attrs
)
{
const
new_attrs
=
this
.
getUpdatedMessageAttributes
(
message
,
attrs
);
new_attrs
&&
message
.
save
(
new_attrs
);
},
/**
* Mutator for setting the chat state of this chat session.
* Handles clearing of any chat state notification timeouts and
* setting new ones if necessary.
* Timeouts are set when the state being set is COMPOSING or PAUSED.
* After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
* See XEP-0085 Chat State Notifications.
* @private
* @method _converse.ChatBox#setChatState
* @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
*/
setChatState
(
state
,
options
)
{
if
(
this
.
chat_state_timeout
!==
undefined
)
{
window
.
clearTimeout
(
this
.
chat_state_timeout
);
delete
this
.
chat_state_timeout
;
}
if
(
state
===
_converse
.
COMPOSING
)
{
this
.
chat_state_timeout
=
window
.
setTimeout
(
this
.
setChatState
.
bind
(
this
),
_converse
.
TIMEOUTS
.
PAUSED
,
_converse
.
PAUSED
);
}
else
if
(
state
===
_converse
.
PAUSED
)
{
this
.
chat_state_timeout
=
window
.
setTimeout
(
this
.
setChatState
.
bind
(
this
),
_converse
.
TIMEOUTS
.
INACTIVE
,
_converse
.
INACTIVE
);
}
this
.
set
(
'
chat_state
'
,
state
,
options
);
return
this
;
},
/**
* Given an error `<message>` stanza's attributes, find the saved message model which is
* referenced by that error.
* @param { Object } attrs
*/
getMessageReferencedByError
(
attrs
)
{
const
id
=
attrs
.
msgid
;
return
id
&&
this
.
messages
.
models
.
find
(
m
=>
[
m
.
get
(
'
msgid
'
),
m
.
get
(
'
retraction_id
'
)].
includes
(
id
));
},
/**
* @private
* @method _converse.ChatBox#shouldShowErrorMessage
* @returns {boolean}
*/
shouldShowErrorMessage
(
attrs
)
{
const
msg
=
this
.
getMessageReferencedByError
(
attrs
);
if
(
!
msg
&&
!
attrs
.
body
)
{
// If the error refers to a message not included in our store,
// and it doesn't have a <body> tag, we assume that this was a
// CSI message (which we don't store).
// See https://github.com/conversejs/converse.js/issues/1317
return
;
}
// Gets overridden in ChatRoom
return
true
;
},
isSameUser
(
jid1
,
jid2
)
{
return
u
.
isSameBareJID
(
jid1
,
jid2
);
},
/**
* Looks whether we already have a retraction for this
* incoming message. If so, it's considered "dangling" because it
* probably hasn't been applied to anything yet, given that the
* relevant message is only coming in now.
* @private
* @method _converse.ChatBox#findDanglingRetraction
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns { _converse.Message }
*/
findDanglingRetraction
(
attrs
)
{
if
(
!
attrs
.
origin_id
||
!
this
.
messages
.
length
)
{
return
null
;
}
// Only look for dangling retractions if there are newer
// messages than this one, since retractions come after.
if
(
this
.
messages
.
last
().
get
(
'
time
'
)
>
attrs
.
time
)
{
// Search from latest backwards
const
messages
=
Array
.
from
(
this
.
messages
.
models
);
messages
.
reverse
();
return
messages
.
find
(
({
attributes
})
=>
attributes
.
retracted_id
===
attrs
.
origin_id
&&
attributes
.
from
===
attrs
.
from
&&
!
attributes
.
moderated_by
);
}
},
/**
* Handles message retraction based on the passed in attributes.
* @private
* @method _converse.ChatBox#handleRetraction
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns { Boolean } Returns `true` or `false` depending on
* whether a message was retracted or not.
*/
async
handleRetraction
(
attrs
)
{
const
RETRACTION_ATTRIBUTES
=
[
'
retracted
'
,
'
retracted_id
'
,
'
editable
'
];
if
(
attrs
.
retracted
)
{
if
(
attrs
.
is_tombstone
)
{
return
false
;
}
const
message
=
this
.
messages
.
findWhere
({
'
origin_id
'
:
attrs
.
retracted_id
,
'
from
'
:
attrs
.
from
});
if
(
!
message
)
{
attrs
[
'
dangling_retraction
'
]
=
true
;
await
this
.
createMessage
(
attrs
);
return
true
;
}
message
.
save
(
pick
(
attrs
,
RETRACTION_ATTRIBUTES
));
return
true
;
}
else
{
// Check if we have dangling retraction
const
message
=
this
.
findDanglingRetraction
(
attrs
);
if
(
message
)
{
const
retraction_attrs
=
pick
(
message
.
attributes
,
RETRACTION_ATTRIBUTES
);
const
new_attrs
=
Object
.
assign
({
'
dangling_retraction
'
:
false
},
attrs
,
retraction_attrs
);
delete
new_attrs
[
'
id
'
];
// Delete id, otherwise a new cache entry gets created
message
.
save
(
new_attrs
);
return
true
;
}
}
return
false
;
},
/**
* Determines whether the passed in message attributes represent a
* message which corrects a previously received message, or an
* older message which has already been corrected.
* In both cases, update the corrected message accordingly.
* @private
* @method _converse.ChatBox#handleCorrection
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns { _converse.Message|undefined } Returns the corrected
* message or `undefined` if not applicable.
*/
handleCorrection
(
attrs
)
{
if
(
!
attrs
.
replace_id
||
!
attrs
.
from
)
{
return
;
}
const
message
=
this
.
messages
.
findWhere
({
'
msgid
'
:
attrs
.
replace_id
,
'
from
'
:
attrs
.
from
});
if
(
!
message
)
{
return
;
}
const
older_versions
=
message
.
get
(
'
older_versions
'
)
||
{};
if
((
attrs
.
time
<
message
.
get
(
'
time
'
))
&&
message
.
get
(
'
edited
'
))
{
// This is an older message which has been corrected afterwards
older_versions
[
attrs
.
time
]
=
attrs
[
'
message
'
];
message
.
save
({
'
older_versions
'
:
older_versions
});
}
else
{
// This is a correction of an earlier message we already received
if
(
Object
.
keys
(
older_versions
).
length
)
{
older_versions
[
message
.
get
(
'
edited
'
)]
=
message
.
get
(
'
message
'
);
}
else
{
older_versions
[
message
.
get
(
'
time
'
)]
=
message
.
get
(
'
message
'
);
}
attrs
=
Object
.
assign
(
attrs
,
{
'
older_versions
'
:
older_versions
});
delete
attrs
[
'
id
'
];
// Delete id, otherwise a new cache entry gets created
attrs
[
'
time
'
]
=
message
.
get
(
'
time
'
);
message
.
save
(
attrs
);
}
return
message
;
},
/**
* Returns an already cached message (if it exists) based on the
* passed in attributes map.
* @private
* @method _converse.ChatBox#getDuplicateMessage
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns {Promise<_converse.Message>}
*/
getDuplicateMessage
(
attrs
)
{
const
queries
=
[
...
this
.
getStanzaIdQueryAttrs
(
attrs
),
this
.
getOriginIdQueryAttrs
(
attrs
),
this
.
getMessageBodyQueryAttrs
(
attrs
)
].
filter
(
s
=>
s
);
const
msgs
=
this
.
messages
.
models
;
return
find
(
msgs
,
m
=>
queries
.
reduce
((
out
,
q
)
=>
(
out
||
isMatch
(
m
.
attributes
,
q
)),
false
));
},
getOriginIdQueryAttrs
(
attrs
)
{
return
attrs
.
origin_id
&&
{
'
origin_id
'
:
attrs
.
origin_id
,
'
from
'
:
attrs
.
from
};
},
getStanzaIdQueryAttrs
(
attrs
)
{
const
keys
=
Object
.
keys
(
attrs
).
filter
(
k
=>
k
.
startsWith
(
'
stanza_id
'
));
return
keys
.
map
(
key
=>
{
const
by_jid
=
key
.
replace
(
/^stanza_id /
,
''
);
const
query
=
{};
query
[
`stanza_id
${
by_jid
}
`
]
=
attrs
[
key
];
return
query
;
});
},
getMessageBodyQueryAttrs
(
attrs
)
{
if
(
attrs
.
message
&&
attrs
.
msgid
)
{
const
query
=
{
'
from
'
:
attrs
.
from
,
'
msgid
'
:
attrs
.
msgid
}
if
(
!
attrs
.
is_encrypted
)
{
// We can't match the message if it's a reflected
// encrypted message (e.g. via MAM or in a MUC)
query
[
'
message
'
]
=
attrs
.
message
;
}
return
query
;
}
},
/**
* Retract one of your messages in this chat
* @private
* @method _converse.ChatBoxView#retractOwnMessage
* @param { _converse.Message } message - The message which we're retracting.
*/
retractOwnMessage
(
message
)
{
this
.
sendRetractionMessage
(
message
)
message
.
save
({
'
retracted
'
:
(
new
Date
()).
toISOString
(),
'
retracted_id
'
:
message
.
get
(
'
origin_id
'
),
'
retraction_id
'
:
message
.
get
(
'
id
'
),
'
is_ephemeral
'
:
true
,
'
editable
'
:
false
});
},
/**
* Sends a message stanza to retract a message in this chat
* @private
* @method _converse.ChatBox#sendRetractionMessage
* @param { _converse.Message } message - The message which we're retracting.
*/
sendRetractionMessage
(
message
)
{
const
origin_id
=
message
.
get
(
'
origin_id
'
);
if
(
!
origin_id
)
{
throw
new
Error
(
"
Can't retract message without a XEP-0359 Origin ID
"
);
}
const
msg
=
$msg
({
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
"
chat
"
})
.
c
(
'
store
'
,
{
xmlns
:
Strophe
.
NS
.
HINTS
}).
up
()
.
c
(
"
apply-to
"
,
{
'
id
'
:
origin_id
,
'
xmlns
'
:
Strophe
.
NS
.
FASTEN
}).
c
(
'
retract
'
,
{
xmlns
:
Strophe
.
NS
.
RETRACT
})
return
_converse
.
connection
.
send
(
msg
);
},
sendMarkerForMessage
(
msg
)
{
if
(
msg
?.
get
(
'
is_markable
'
))
{
const
from_jid
=
Strophe
.
getBareJidFromJid
(
msg
.
get
(
'
from
'
));
this
.
sendMarker
(
from_jid
,
msg
.
get
(
'
msgid
'
),
'
displayed
'
,
msg
.
get
(
'
type
'
));
}
},
sendMarker
(
to_jid
,
id
,
type
,
msg_type
)
{
const
stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
to_jid
,
'
type
'
:
msg_type
?
msg_type
:
'
chat
'
}).
c
(
type
,
{
'
xmlns
'
:
Strophe
.
NS
.
MARKERS
,
'
id
'
:
id
});
api
.
send
(
stanza
);
},
handleChatMarker
(
attrs
)
{
const
to_bare_jid
=
Strophe
.
getBareJidFromJid
(
attrs
.
to
);
if
(
to_bare_jid
!==
_converse
.
bare_jid
)
{
return
false
;
}
if
(
attrs
.
is_markable
)
{
if
(
this
.
contact
&&
!
attrs
.
is_archived
&&
!
attrs
.
is_carbon
)
{
this
.
sendMarker
(
attrs
.
from
,
attrs
.
msgid
,
'
received
'
);
}
return
false
;
}
else
if
(
attrs
.
marker_id
)
{
const
message
=
this
.
messages
.
findWhere
({
'
msgid
'
:
attrs
.
marker_id
});
const
field_name
=
`marker_
${
attrs
.
marker
}
`
;
if
(
message
&&
!
message
.
get
(
field_name
))
{
message
.
save
({
field_name
:
(
new
Date
()).
toISOString
()});
}
return
true
;
}
},
sendReceiptStanza
(
to_jid
,
id
)
{
const
receipt_stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
to_jid
,
'
type
'
:
'
chat
'
,
}).
c
(
'
received
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
RECEIPTS
,
'
id
'
:
id
}).
up
()
.
c
(
'
store
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HINTS
}).
up
();
api
.
send
(
receipt_stanza
);
},
handleReceipt
(
attrs
)
{
if
(
attrs
.
sender
===
'
them
'
)
{
if
(
attrs
.
is_valid_receipt_request
)
{
this
.
sendReceiptStanza
(
attrs
.
from
,
attrs
.
msgid
);
}
else
if
(
attrs
.
receipt_id
)
{
const
message
=
this
.
messages
.
findWhere
({
'
msgid
'
:
attrs
.
receipt_id
});
if
(
message
&&
!
message
.
get
(
'
received
'
))
{
message
.
save
({
'
received
'
:
(
new
Date
()).
toISOString
()});
}
return
true
;
}
}
return
false
;
},
/**
* Given a {@link _converse.Message} return the XML stanza that represents it.
* @private
* @method _converse.ChatBox#createMessageStanza
* @param { _converse.Message } message - The message object
*/
createMessageStanza
(
message
)
{
const
stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
this
.
get
(
'
message_type
'
),
'
id
'
:
message
.
get
(
'
edited
'
)
&&
u
.
getUniqueId
()
||
message
.
get
(
'
msgid
'
),
}).
c
(
'
body
'
).
t
(
message
.
get
(
'
message
'
)).
up
()
.
c
(
_converse
.
ACTIVE
,
{
'
xmlns
'
:
Strophe
.
NS
.
CHATSTATES
}).
root
();
if
(
message
.
get
(
'
type
'
)
===
'
chat
'
)
{
stanza
.
c
(
'
request
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
RECEIPTS
}).
root
();
}
if
(
message
.
get
(
'
is_spoiler
'
))
{
if
(
message
.
get
(
'
spoiler_hint
'
))
{
stanza
.
c
(
'
spoiler
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
SPOILER
},
message
.
get
(
'
spoiler_hint
'
)).
root
();
}
else
{
stanza
.
c
(
'
spoiler
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
SPOILER
}).
root
();
}
}
(
message
.
get
(
'
references
'
)
||
[]).
forEach
(
reference
=>
{
const
attrs
=
{
'
xmlns
'
:
Strophe
.
NS
.
REFERENCE
,
'
begin
'
:
reference
.
begin
,
'
end
'
:
reference
.
end
,
'
type
'
:
reference
.
type
,
}
if
(
reference
.
uri
)
{
attrs
.
uri
=
reference
.
uri
;
}
stanza
.
c
(
'
reference
'
,
attrs
).
root
();
});
if
(
message
.
get
(
'
oob_url
'
))
{
stanza
.
c
(
'
x
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
OUTOFBAND
}).
c
(
'
url
'
).
t
(
message
.
get
(
'
oob_url
'
)).
root
();
}
if
(
message
.
get
(
'
edited
'
))
{
stanza
.
c
(
'
replace
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
MESSAGE_CORRECT
,
'
id
'
:
message
.
get
(
'
msgid
'
)
}).
root
();
}
if
(
message
.
get
(
'
origin_id
'
))
{
stanza
.
c
(
'
origin-id
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
SID
,
'
id
'
:
message
.
get
(
'
origin_id
'
)}).
root
();
}
return
stanza
;
},
getOutgoingMessageAttributes
(
text
,
spoiler_hint
)
{
const
is_spoiler
=
this
.
get
(
'
composing_spoiler
'
);
const
origin_id
=
u
.
getUniqueId
();
const
body
=
text
?
u
.
httpToGeoUri
(
u
.
shortnamesToUnicode
(
text
),
_converse
)
:
undefined
;
return
{
'
from
'
:
_converse
.
bare_jid
,
'
fullname
'
:
_converse
.
xmppstatus
.
get
(
'
fullname
'
),
'
id
'
:
origin_id
,
'
is_only_emojis
'
:
text
?
u
.
isOnlyEmojis
(
text
)
:
false
,
'
jid
'
:
this
.
get
(
'
jid
'
),
'
message
'
:
body
,
'
msgid
'
:
origin_id
,
'
nickname
'
:
this
.
get
(
'
nickname
'
),
'
sender
'
:
'
me
'
,
'
spoiler_hint
'
:
is_spoiler
?
spoiler_hint
:
undefined
,
'
time
'
:
(
new
Date
()).
toISOString
(),
'
type
'
:
this
.
get
(
'
message_type
'
),
body
,
is_spoiler
,
origin_id
}
},
/**
* Responsible for setting the editable attribute of messages.
* If api.settings.get('allow_message_corrections') is "last", then only the last
* message sent from me will be editable. If set to "all" all messages
* will be editable. Otherwise no messages will be editable.
* @method _converse.ChatBox#setEditable
* @memberOf _converse.ChatBox
* @param { Object } attrs An object containing message attributes.
* @param { String } send_time - time when the message was sent
*/
setEditable
(
attrs
,
send_time
)
{
if
(
attrs
.
is_headline
||
u
.
isEmptyMessage
(
attrs
)
||
attrs
.
sender
!==
'
me
'
)
{
return
;
}
if
(
api
.
settings
.
get
(
'
allow_message_corrections
'
)
===
'
all
'
)
{
attrs
.
editable
=
!
(
attrs
.
file
||
attrs
.
retracted
||
'
oob_url
'
in
attrs
);
}
else
if
((
api
.
settings
.
get
(
'
allow_message_corrections
'
)
===
'
last
'
)
&&
(
send_time
>
this
.
get
(
'
time_sent
'
)))
{
this
.
set
({
'
time_sent
'
:
send_time
});
const
msg
=
this
.
messages
.
findWhere
({
'
editable
'
:
true
});
if
(
msg
)
{
msg
.
save
({
'
editable
'
:
false
});
}
attrs
.
editable
=
!
(
attrs
.
file
||
attrs
.
retracted
||
'
oob_url
'
in
attrs
);
}
},
/**
* Queue the creation of a message, to make sure that we don't run
* into a race condition whereby we're creating a new message
* before the collection has been fetched.
* @async
* @private
* @method _converse.ChatRoom#queueMessageCreation
* @param { Object } attrs
*/
async
createMessage
(
attrs
,
options
)
{
attrs
.
time
=
attrs
.
time
||
(
new
Date
()).
toISOString
();
await
this
.
messages
.
fetched
;
const
p
=
this
.
messages
.
create
(
attrs
,
Object
.
assign
({
'
wait
'
:
true
,
'
promise
'
:
true
},
options
));
return
p
;
},
/**
* Responsible for sending off a text message inside an ongoing chat conversation.
* @private
* @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox
* @param { String } text - The chat message text
* @param { String } spoiler_hint - An optional hint, if the message being sent is a spoiler
* @returns { _converse.Message }
* @example
* const chat = api.chats.get('buddy1@example.com');
* chat.sendMessage('hello world');
*/
async
sendMessage
(
text
,
spoiler_hint
)
{
const
attrs
=
this
.
getOutgoingMessageAttributes
(
text
,
spoiler_hint
);
let
message
=
this
.
messages
.
findWhere
(
'
correcting
'
)
if
(
message
)
{
const
older_versions
=
message
.
get
(
'
older_versions
'
)
||
{};
older_versions
[
message
.
get
(
'
time
'
)]
=
message
.
get
(
'
message
'
);
message
.
save
({
'
correcting
'
:
false
,
'
edited
'
:
(
new
Date
()).
toISOString
(),
'
message
'
:
attrs
.
message
,
'
older_versions
'
:
older_versions
,
'
references
'
:
attrs
.
references
,
'
is_only_emojis
'
:
attrs
.
is_only_emojis
,
'
origin_id
'
:
u
.
getUniqueId
(),
'
received
'
:
undefined
});
}
else
{
this
.
setEditable
(
attrs
,
(
new
Date
()).
toISOString
());
message
=
await
this
.
createMessage
(
attrs
);
}
api
.
send
(
this
.
createMessageStanza
(
message
));
/**
* Triggered when a message is being sent out
* @event _converse#sendMessage
* @type { Object }
* @param { Object } data
* @property { (_converse.ChatBox | _converse.ChatRoom) } data.chatbox
* @property { (_converse.Message | _converse.ChatRoomMessage) } data.message
*/
api
.
trigger
(
'
sendMessage
'
,
{
'
chatbox
'
:
this
,
message
});
return
message
;
},
/**
* Sends a message with the current XEP-0085 chat state of the user
* as taken from the `chat_state` attribute of the {@link _converse.ChatBox}.
* @private
* @method _converse.ChatBox#sendChatState
*/
sendChatState
()
{
if
(
api
.
settings
.
get
(
'
send_chat_state_notifications
'
)
&&
this
.
get
(
'
chat_state
'
))
{
const
allowed
=
api
.
settings
.
get
(
'
send_chat_state_notifications
'
);
if
(
Array
.
isArray
(
allowed
)
&&
!
allowed
.
includes
(
this
.
get
(
'
chat_state
'
)))
{
return
;
}
api
.
send
(
$msg
({
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
'
chat
'
}).
c
(
this
.
get
(
'
chat_state
'
),
{
'
xmlns
'
:
Strophe
.
NS
.
CHATSTATES
}).
up
()
.
c
(
'
no-store
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HINTS
}).
up
()
.
c
(
'
no-permanent-store
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HINTS
})
);
}
},
async
sendFiles
(
files
)
{
const
result
=
await
api
.
disco
.
features
.
get
(
Strophe
.
NS
.
HTTPUPLOAD
,
_converse
.
domain
);
const
item
=
result
.
pop
();
if
(
!
item
)
{
this
.
createMessage
({
'
message
'
:
__
(
"
Sorry, looks like file upload is not supported by your server.
"
),
'
type
'
:
'
error
'
,
'
is_ephemeral
'
:
true
});
return
;
}
const
data
=
item
.
dataforms
.
where
({
'
FORM_TYPE
'
:
{
'
value
'
:
Strophe
.
NS
.
HTTPUPLOAD
,
'
type
'
:
"
hidden
"
}}).
pop
();
const
max_file_size
=
window
.
parseInt
((
data
?.
attributes
||
{})[
'
max-file-size
'
]?.
value
);
const
slot_request_url
=
item
?.
id
;
if
(
!
slot_request_url
)
{
this
.
createMessage
({
'
message
'
:
__
(
"
Sorry, looks like file upload is not supported by your server.
"
),
'
type
'
:
'
error
'
,
'
is_ephemeral
'
:
true
});
return
;
}
Array
.
from
(
files
).
forEach
(
async
file
=>
{
if
(
!
window
.
isNaN
(
max_file_size
)
&&
window
.
parseInt
(
file
.
size
)
>
max_file_size
)
{
return
this
.
createMessage
({
'
message
'
:
__
(
'
The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.
'
,
file
.
name
,
filesize
(
max_file_size
)),
'
type
'
:
'
error
'
,
'
is_ephemeral
'
:
true
});
}
else
{
const
attrs
=
Object
.
assign
(
this
.
getOutgoingMessageAttributes
(),
{
'
file
'
:
true
,
'
progress
'
:
0
,
'
slot_request_url
'
:
slot_request_url
});
this
.
setEditable
(
attrs
,
(
new
Date
()).
toISOString
());
const
message
=
await
this
.
createMessage
(
attrs
,
{
'
silent
'
:
true
});
message
.
file
=
file
;
this
.
messages
.
trigger
(
'
add
'
,
message
);
message
.
getRequestSlotURL
();
}
});
},
maybeShow
(
force
)
{
if
(
force
)
{
if
(
_converse
.
isUniView
())
{
// We only have one chat visible at any one time.
// So before opening a chat, we make sure all other chats are hidden.
const
filter
=
c
=>
!
c
.
get
(
'
hidden
'
)
&&
c
.
get
(
'
jid
'
)
!==
this
.
get
(
'
jid
'
)
&&
c
.
get
(
'
id
'
)
!==
'
controlbox
'
;
_converse
.
chatboxes
.
filter
(
filter
).
forEach
(
c
=>
u
.
safeSave
(
c
,
{
'
hidden
'
:
true
}));
}
u
.
safeSave
(
this
,
{
'
hidden
'
:
false
});
}
if
(
_converse
.
isUniView
()
&&
this
.
get
(
'
hidden
'
))
{
return
;
}
else
{
return
this
.
trigger
(
"
show
"
);
}
},
/**
* Indicates whether the chat is hidden and therefore
* whether a newly received message will be visible
* to the user or not.
* @returns {boolean}
*/
isHidden
()
{
// Note: This methods gets overridden by converse-minimize
const
hidden
=
_converse
.
isUniView
()
&&
this
.
get
(
'
hidden
'
);
return
hidden
||
this
.
isScrolledUp
()
||
_converse
.
windowState
===
'
hidden
'
;
},
/**
* Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary.
* @private
* @param {_converse.Message} message
*/
handleUnreadMessage
(
message
)
{
if
(
!
message
?.
get
(
'
body
'
))
{
return
}
if
(
utils
.
isNewMessage
(
message
))
{
if
(
this
.
isHidden
())
{
const
settings
=
{
'
num_unread
'
:
this
.
get
(
'
num_unread
'
)
+
1
};
if
(
this
.
get
(
'
num_unread
'
)
===
0
)
{
settings
[
'
first_unread_id
'
]
=
message
.
get
(
'
id
'
);
}
this
.
save
(
settings
);
}
else
{
this
.
sendMarkerForMessage
(
message
);
}
}
},
clearUnreadMsgCounter
()
{
if
(
this
.
get
(
'
num_unread
'
)
>
0
)
{
this
.
sendMarkerForMessage
(
this
.
messages
.
last
());
}
u
.
safeSave
(
this
,
{
'
num_unread
'
:
0
});
},
isScrolledUp
()
{
return
this
.
get
(
'
scrolled
'
,
true
);
}
});
async
function
handleErrorMessage
(
stanza
)
{
const
from_jid
=
Strophe
.
getBareJidFromJid
(
stanza
.
getAttribute
(
'
from
'
));
if
(
utils
.
isSameBareJID
(
from_jid
,
_converse
.
bare_jid
))
{
return
;
}
const
chatbox
=
await
api
.
chatboxes
.
get
(
from_jid
);
chatbox
?.
handleErrorMessageStanza
(
stanza
);
}
/**
* Handler method for all incoming single-user chat "message" stanzas.
* @private
* @method _converse#handleMessageStanza
* @param { MessageAttributes } attrs - The message attributes
*/
_converse
.
handleMessageStanza
=
async
function
(
stanza
)
{
if
(
st
.
isServerMessage
(
stanza
))
{
// Prosody sends headline messages with type `chat`, so we need to filter them out here.
const
from
=
stanza
.
getAttribute
(
'
from
'
);
return
log
.
info
(
`handleMessageStanza: Ignoring incoming server message from JID:
${
from
}
`
);
}
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
if
(
u
.
isErrorObject
(
attrs
))
{
attrs
.
stanza
&&
log
.
error
(
attrs
.
stanza
);
return
log
.
error
(
attrs
.
message
);
}
const
has_body
=
!!
sizzle
(
`body, encrypted[xmlns="
${
Strophe
.
NS
.
OMEMO
}
"]`
,
stanza
).
length
;
const
chatbox
=
await
api
.
chats
.
get
(
attrs
.
contact_jid
,
{
'
nickname
'
:
attrs
.
nick
},
has_body
);
await
chatbox
?.
queueMessage
(
attrs
);
/**
* @typedef { Object } MessageData
* An object containing the original message stanza, as well as the
* parsed attributes.
* @property { XMLElement } stanza
* @property { MessageAttributes } stanza
* @property { ChatBox } chatbox
*/
const
data
=
{
stanza
,
attrs
,
chatbox
};
/**
* Triggered when a message stanza is been received and processed.
* @event _converse#message
* @type { object }
* @property { module:converse-chat~MessageData } data
*/
api
.
trigger
(
'
message
'
,
data
);
}
function
registerMessageHandlers
()
{
_converse
.
connection
.
addHandler
(
stanza
=>
{
if
(
sizzle
(
`message > result[xmlns="
${
Strophe
.
NS
.
MAM
}
"]`
,
stanza
).
pop
())
{
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
log
.
warn
(
`Received a MAM message with type "chat".`
);
return
true
;
}
_converse
.
handleMessageStanza
(
stanza
);
return
true
;
},
null
,
'
message
'
,
'
chat
'
);
_converse
.
connection
.
addHandler
(
stanza
=>
{
// Message receipts are usually without the `type` attribute. See #1353
if
(
stanza
.
getAttribute
(
'
type
'
)
!==
null
)
{
// TODO: currently Strophe has no way to register a handler
// for stanzas without a `type` attribute.
// We could update it to accept null to mean no attribute,
// but that would be a backward-incompatible change
return
true
;
// Gets handled above.
}
_converse
.
handleMessageStanza
(
stanza
);
return
true
;
},
Strophe
.
NS
.
RECEIPTS
,
'
message
'
);
_converse
.
connection
.
addHandler
(
stanza
=>
{
handleErrorMessage
(
stanza
);
return
true
;
},
null
,
'
message
'
,
'
error
'
);
}
function
autoJoinChats
()
{
// Automatically join private chats, based on the
// "auto_join_private_chats" configuration setting.
api
.
settings
.
get
(
'
auto_join_private_chats
'
).
forEach
(
jid
=>
{
if
(
_converse
.
chatboxes
.
where
({
'
jid
'
:
jid
}).
length
)
{
return
;
}
if
(
typeof
jid
===
'
string
'
)
{
api
.
chats
.
open
(
jid
);
}
else
{
log
.
error
(
'
Invalid jid criteria specified for "auto_join_private_chats"
'
);
}
});
/**
* Triggered once any private chats have been automatically joined as
* specified by the `auto_join_private_chats` setting.
* See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats
* @event _converse#privateChatsAutoJoined
* @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... });
* @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... });
*/
api
.
trigger
(
'
privateChatsAutoJoined
'
);
}
/************************ BEGIN Route Handlers ************************/
function
openChat
(
jid
)
{
if
(
!
utils
.
isValidJID
(
jid
))
{
return
log
.
warn
(
`Invalid JID "
${
jid
}
" provided in URL fragment`
);
}
api
.
chats
.
open
(
jid
);
}
_converse
.
router
.
route
(
'
converse/chat?jid=:jid
'
,
openChat
);
/************************ END Route Handlers ************************/
/************************ BEGIN Event Handlers ************************/
api
.
listen
.
on
(
'
chatBoxesFetched
'
,
autoJoinChats
);
api
.
listen
.
on
(
'
presencesInitialized
'
,
registerMessageHandlers
);
api
.
listen
.
on
(
'
clearSession
'
,
async
()
=>
{
if
(
_converse
.
shouldClearCache
())
{
await
Promise
.
all
(
_converse
.
chatboxes
.
map
(
c
=>
c
.
messages
&&
c
.
messages
.
clearStore
({
'
silent
'
:
true
})));
const
filter
=
(
o
)
=>
(
o
.
get
(
'
type
'
)
!==
_converse
.
CONTROLBOX_TYPE
);
_converse
.
chatboxes
.
clearStore
({
'
silent
'
:
true
},
filter
);
}
});
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
Object
.
assign
(
api
,
{
/**
* The "chats" namespace (used for one-on-one chats)
*
* @namespace api.chats
* @memberOf api
*/
chats
:
{
/**
* @method api.chats.create
* @param {string|string[]} jid|jids An jid or array of jids
* @param {object} [attrs] An object containing configuration attributes.
*/
async
create
(
jids
,
attrs
)
{
if
(
typeof
jids
===
'
string
'
)
{
if
(
attrs
&&
!
attrs
?.
fullname
)
{
const
contact
=
await
api
.
contacts
.
get
(
jids
);
attrs
.
fullname
=
contact
?.
attributes
?.
fullname
;
}
const
chatbox
=
api
.
chats
.
get
(
jids
,
attrs
,
true
);
if
(
!
chatbox
)
{
log
.
error
(
"
Could not open chatbox for JID:
"
+
jids
);
return
;
}
return
chatbox
;
}
if
(
Array
.
isArray
(
jids
))
{
return
Promise
.
all
(
jids
.
forEach
(
async
jid
=>
{
const
contact
=
await
api
.
contacts
.
get
(
jids
);
attrs
.
fullname
=
contact
?.
attributes
?.
fullname
;
return
api
.
chats
.
get
(
jid
,
attrs
,
true
).
maybeShow
();
}));
}
log
.
error
(
"
chats.create: You need to provide at least one JID
"
);
return
null
;
},
/**
* Opens a new one-on-one chat.
*
* @method api.chats.open
* @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [attrs.minimized] - Should the chat be created in minimized state.
* @param {Boolean} [force=false] - By default, a minimized
* chat won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened chat won't replace
* another chat already in the foreground.
* Set `force` to `true` if you want to force the chat to be
* maximized or shown.
* @returns {Promise} Promise which resolves with the
* _converse.ChatBox representing the chat.
*
* @example
* // To open a single chat, provide the JID of the contact you're chatting with in that chat:
* converse.plugins.add('myplugin', {
* initialize: function() {
* const _converse = this._converse;
* // Note, buddy@example.org must be in your contacts roster!
* api.chats.open('buddy@example.com').then(chat => {
* // Now you can do something with the chat model
* });
* }
* });
*
* @example
* // To open an array of chats, provide an array of JIDs:
* converse.plugins.add('myplugin', {
* initialize: function () {
* const _converse = this._converse;
* // Note, these users must first be in your contacts roster!
* api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then(chats => {
* // Now you can do something with the chat models
* });
* }
* });
*/
async
open
(
jids
,
attrs
,
force
)
{
if
(
typeof
jids
===
'
string
'
)
{
const
chat
=
await
api
.
chats
.
get
(
jids
,
attrs
,
true
);
if
(
chat
)
{
return
chat
.
maybeShow
(
force
);
}
return
chat
;
}
else
if
(
Array
.
isArray
(
jids
))
{
return
Promise
.
all
(
jids
.
map
(
j
=>
api
.
chats
.
get
(
j
,
attrs
,
true
).
then
(
c
=>
c
&&
c
.
maybeShow
(
force
)))
.
filter
(
c
=>
c
)
);
}
const
err_msg
=
"
chats.open: You need to provide at least one JID
"
;
log
.
error
(
err_msg
);
throw
new
Error
(
err_msg
);
},
/**
* Retrieves a chat or all chats.
*
* @method api.chats.get
* @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.ChatBox> }
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
* const model = await api.chats.get('buddy@example.com');
*
* @example
* // To return an array of chats, provide an array of JIDs:
* const models = await api.chats.get(['buddy1@example.com', 'buddy2@example.com']);
*
* @example
* // To return all open chats, call the method without any parameters::
* const models = await api.chats.get();
*
*/
async
get
(
jids
,
attrs
=
{},
create
=
false
)
{
async
function
_get
(
jid
)
{
let
model
=
await
api
.
chatboxes
.
get
(
jid
);
if
(
!
model
&&
create
)
{
model
=
await
api
.
chatboxes
.
create
(
jid
,
attrs
,
_converse
.
ChatBox
);
}
else
{
model
=
(
model
&&
model
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
)
?
model
:
null
;
if
(
model
&&
Object
.
keys
(
attrs
).
length
)
{
model
.
save
(
attrs
);
}
}
return
model
;
}
if
(
jids
===
undefined
)
{
const
chats
=
await
api
.
chatboxes
.
get
();
return
chats
.
filter
(
c
=>
(
c
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
));
}
else
if
(
typeof
jids
===
'
string
'
)
{
return
_get
(
jids
);
}
return
Promise
.
all
(
jids
.
map
(
jid
=>
_get
(
jid
)));
}
}
});
/************************ END API ************************/
}
});
src/headless/plugins/chat/api.js
0 → 100644
View file @
e3ebde97
import
{
_converse
,
api
}
from
"
../../core.js
"
;
import
log
from
"
../../log.js
"
;
export
default
{
/**
* The "chats" namespace (used for one-on-one chats)
*
* @namespace api.chats
* @memberOf api
*/
chats
:
{
/**
* @method api.chats.create
* @param {string|string[]} jid|jids An jid or array of jids
* @param {object} [attrs] An object containing configuration attributes.
*/
async
create
(
jids
,
attrs
)
{
if
(
typeof
jids
===
'
string
'
)
{
if
(
attrs
&&
!
attrs
?.
fullname
)
{
const
contact
=
await
api
.
contacts
.
get
(
jids
);
attrs
.
fullname
=
contact
?.
attributes
?.
fullname
;
}
const
chatbox
=
api
.
chats
.
get
(
jids
,
attrs
,
true
);
if
(
!
chatbox
)
{
log
.
error
(
"
Could not open chatbox for JID:
"
+
jids
);
return
;
}
return
chatbox
;
}
if
(
Array
.
isArray
(
jids
))
{
return
Promise
.
all
(
jids
.
forEach
(
async
jid
=>
{
const
contact
=
await
api
.
contacts
.
get
(
jids
);
attrs
.
fullname
=
contact
?.
attributes
?.
fullname
;
return
api
.
chats
.
get
(
jid
,
attrs
,
true
).
maybeShow
();
}));
}
log
.
error
(
"
chats.create: You need to provide at least one JID
"
);
return
null
;
},
/**
* Opens a new one-on-one chat.
*
* @method api.chats.open
* @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [attrs.minimized] - Should the chat be created in minimized state.
* @param {Boolean} [force=false] - By default, a minimized
* chat won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened chat won't replace
* another chat already in the foreground.
* Set `force` to `true` if you want to force the chat to be
* maximized or shown.
* @returns {Promise} Promise which resolves with the
* _converse.ChatBox representing the chat.
*
* @example
* // To open a single chat, provide the JID of the contact you're chatting with in that chat:
* converse.plugins.add('myplugin', {
* initialize: function() {
* const _converse = this._converse;
* // Note, buddy@example.org must be in your contacts roster!
* api.chats.open('buddy@example.com').then(chat => {
* // Now you can do something with the chat model
* });
* }
* });
*
* @example
* // To open an array of chats, provide an array of JIDs:
* converse.plugins.add('myplugin', {
* initialize: function () {
* const _converse = this._converse;
* // Note, these users must first be in your contacts roster!
* api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then(chats => {
* // Now you can do something with the chat models
* });
* }
* });
*/
async
open
(
jids
,
attrs
,
force
)
{
if
(
typeof
jids
===
'
string
'
)
{
const
chat
=
await
api
.
chats
.
get
(
jids
,
attrs
,
true
);
if
(
chat
)
{
return
chat
.
maybeShow
(
force
);
}
return
chat
;
}
else
if
(
Array
.
isArray
(
jids
))
{
return
Promise
.
all
(
jids
.
map
(
j
=>
api
.
chats
.
get
(
j
,
attrs
,
true
).
then
(
c
=>
c
&&
c
.
maybeShow
(
force
)))
.
filter
(
c
=>
c
)
);
}
const
err_msg
=
"
chats.open: You need to provide at least one JID
"
;
log
.
error
(
err_msg
);
throw
new
Error
(
err_msg
);
},
/**
* Retrieves a chat or all chats.
*
* @method api.chats.get
* @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.ChatBox> }
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
* const model = await api.chats.get('buddy@example.com');
*
* @example
* // To return an array of chats, provide an array of JIDs:
* const models = await api.chats.get(['buddy1@example.com', 'buddy2@example.com']);
*
* @example
* // To return all open chats, call the method without any parameters::
* const models = await api.chats.get();
*
*/
async
get
(
jids
,
attrs
=
{},
create
=
false
)
{
async
function
_get
(
jid
)
{
let
model
=
await
api
.
chatboxes
.
get
(
jid
);
if
(
!
model
&&
create
)
{
model
=
await
api
.
chatboxes
.
create
(
jid
,
attrs
,
_converse
.
ChatBox
);
}
else
{
model
=
(
model
&&
model
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
)
?
model
:
null
;
if
(
model
&&
Object
.
keys
(
attrs
).
length
)
{
model
.
save
(
attrs
);
}
}
return
model
;
}
if
(
jids
===
undefined
)
{
const
chats
=
await
api
.
chatboxes
.
get
();
return
chats
.
filter
(
c
=>
(
c
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
));
}
else
if
(
typeof
jids
===
'
string
'
)
{
return
_get
(
jids
);
}
return
Promise
.
all
(
jids
.
map
(
jid
=>
_get
(
jid
)));
}
}
}
src/headless/plugins/chat/index.js
0 → 100644
View file @
e3ebde97
/**
* @module converse-chat
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import
ChatBox
from
'
./model.js
'
;
import
MessageMixin
from
'
./message.js
'
;
import
ModelWithContact
from
'
./model-with-contact.js
'
;
import
chat_api
from
'
./api.js
'
;
import
log
from
'
../../log.js
'
;
import
st
from
'
../../utils/stanza
'
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
import
{
_converse
,
api
,
converse
}
from
'
../../core.js
'
;
const
{
Strophe
,
sizzle
,
utils
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
async
function
handleErrorMessage
(
stanza
)
{
const
from_jid
=
Strophe
.
getBareJidFromJid
(
stanza
.
getAttribute
(
'
from
'
));
if
(
utils
.
isSameBareJID
(
from_jid
,
_converse
.
bare_jid
))
{
return
;
}
const
chatbox
=
await
api
.
chatboxes
.
get
(
from_jid
);
chatbox
?.
handleErrorMessageStanza
(
stanza
);
}
converse
.
plugins
.
add
(
'
converse-chat
'
,
{
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies
:
[
'
converse-chatboxes
'
,
'
converse-disco
'
],
initialize
()
{
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
Object
.
assign
(
api
,
chat_api
);
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
api
.
settings
.
extend
({
'
allow_message_corrections
'
:
'
all
'
,
'
allow_message_retraction
'
:
'
all
'
,
'
allow_message_styling
'
:
true
,
'
auto_join_private_chats
'
:
[],
'
clear_messages_on_reconnection
'
:
false
,
'
filter_by_resource
'
:
false
,
'
send_chat_state_notifications
'
:
true
});
_converse
.
Message
=
ModelWithContact
.
extend
(
MessageMixin
);
_converse
.
Messages
=
Collection
.
extend
({
model
:
_converse
.
Message
,
comparator
:
'
time
'
});
_converse
.
ChatBox
=
ChatBox
;
/**
* Handler method for all incoming single-user chat "message" stanzas.
* @private
* @method _converse#handleMessageStanza
* @param { MessageAttributes } attrs - The message attributes
*/
_converse
.
handleMessageStanza
=
async
function
(
stanza
)
{
if
(
st
.
isServerMessage
(
stanza
))
{
// Prosody sends headline messages with type `chat`, so we need to filter them out here.
const
from
=
stanza
.
getAttribute
(
'
from
'
);
return
log
.
info
(
`handleMessageStanza: Ignoring incoming server message from JID:
${
from
}
`
);
}
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
if
(
u
.
isErrorObject
(
attrs
))
{
attrs
.
stanza
&&
log
.
error
(
attrs
.
stanza
);
return
log
.
error
(
attrs
.
message
);
}
const
has_body
=
!!
sizzle
(
`body, encrypted[xmlns="
${
Strophe
.
NS
.
OMEMO
}
"]`
,
stanza
).
length
;
const
chatbox
=
await
api
.
chats
.
get
(
attrs
.
contact_jid
,
{
'
nickname
'
:
attrs
.
nick
},
has_body
);
await
chatbox
?.
queueMessage
(
attrs
);
/**
* @typedef { Object } MessageData
* An object containing the original message stanza, as well as the
* parsed attributes.
* @property { XMLElement } stanza
* @property { MessageAttributes } stanza
* @property { ChatBox } chatbox
*/
const
data
=
{
stanza
,
attrs
,
chatbox
};
/**
* Triggered when a message stanza is been received and processed.
* @event _converse#message
* @type { object }
* @property { module:converse-chat~MessageData } data
*/
api
.
trigger
(
'
message
'
,
data
);
};
function
registerMessageHandlers
()
{
_converse
.
connection
.
addHandler
(
stanza
=>
{
if
(
sizzle
(
`message > result[xmlns="
${
Strophe
.
NS
.
MAM
}
"]`
,
stanza
).
pop
())
{
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
log
.
warn
(
`Received a MAM message with type "chat".`
);
return
true
;
}
_converse
.
handleMessageStanza
(
stanza
);
return
true
;
},
null
,
'
message
'
,
'
chat
'
);
_converse
.
connection
.
addHandler
(
stanza
=>
{
// Message receipts are usually without the `type` attribute. See #1353
if
(
stanza
.
getAttribute
(
'
type
'
)
!==
null
)
{
// TODO: currently Strophe has no way to register a handler
// for stanzas without a `type` attribute.
// We could update it to accept null to mean no attribute,
// but that would be a backward-incompatible change
return
true
;
// Gets handled above.
}
_converse
.
handleMessageStanza
(
stanza
);
return
true
;
},
Strophe
.
NS
.
RECEIPTS
,
'
message
'
);
_converse
.
connection
.
addHandler
(
stanza
=>
{
handleErrorMessage
(
stanza
);
return
true
;
},
null
,
'
message
'
,
'
error
'
);
}
function
autoJoinChats
()
{
// Automatically join private chats, based on the
// "auto_join_private_chats" configuration setting.
api
.
settings
.
get
(
'
auto_join_private_chats
'
).
forEach
(
jid
=>
{
if
(
_converse
.
chatboxes
.
where
({
'
jid
'
:
jid
}).
length
)
{
return
;
}
if
(
typeof
jid
===
'
string
'
)
{
api
.
chats
.
open
(
jid
);
}
else
{
log
.
error
(
'
Invalid jid criteria specified for "auto_join_private_chats"
'
);
}
});
/**
* Triggered once any private chats have been automatically joined as
* specified by the `auto_join_private_chats` setting.
* See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats
* @event _converse#privateChatsAutoJoined
* @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... });
* @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... });
*/
api
.
trigger
(
'
privateChatsAutoJoined
'
);
}
/************************ BEGIN Route Handlers ************************/
function
openChat
(
jid
)
{
if
(
!
utils
.
isValidJID
(
jid
))
{
return
log
.
warn
(
`Invalid JID "
${
jid
}
" provided in URL fragment`
);
}
api
.
chats
.
open
(
jid
);
}
_converse
.
router
.
route
(
'
converse/chat?jid=:jid
'
,
openChat
);
/************************ END Route Handlers ************************/
/************************ BEGIN Event Handlers ************************/
api
.
listen
.
on
(
'
chatBoxesFetched
'
,
autoJoinChats
);
api
.
listen
.
on
(
'
presencesInitialized
'
,
registerMessageHandlers
);
api
.
listen
.
on
(
'
clearSession
'
,
async
()
=>
{
if
(
_converse
.
shouldClearCache
())
{
await
Promise
.
all
(
_converse
.
chatboxes
.
map
(
c
=>
c
.
messages
&&
c
.
messages
.
clearStore
({
'
silent
'
:
true
}))
);
const
filter
=
o
=>
o
.
get
(
'
type
'
)
!==
_converse
.
CONTROLBOX_TYPE
;
_converse
.
chatboxes
.
clearStore
({
'
silent
'
:
true
},
filter
);
}
});
/************************ END Event Handlers ************************/
}
});
src/headless/plugins/chat/message.js
0 → 100644
View file @
e3ebde97
import
ModelWithContact
from
'
./model-with-contact.js
'
;
import
log
from
'
../../log.js
'
;
import
{
_converse
,
api
,
converse
}
from
'
../../core.js
'
;
const
u
=
converse
.
env
.
utils
;
const
{
Strophe
}
=
converse
.
env
;
/**
* Mixin which turns a `ModelWithContact` model into a non-MUC message. These can be either `chat` messages or `headline` messages.
* @mixin
* @namespace _converse.Message
* @memberOf _converse
* @example const msg = new _converse.Message({'message': 'hello world!'});
*/
const
MessageMixin
=
{
defaults
()
{
return
{
'
msgid
'
:
u
.
getUniqueId
(),
'
time
'
:
new
Date
().
toISOString
(),
'
is_ephemeral
'
:
false
};
},
async
initialize
()
{
if
(
!
this
.
checkValidity
())
{
return
;
}
this
.
initialized
=
u
.
getResolveablePromise
();
if
(
this
.
get
(
'
type
'
)
===
'
chat
'
)
{
ModelWithContact
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
this
.
setRosterContact
(
Strophe
.
getBareJidFromJid
(
this
.
get
(
'
from
'
)));
}
if
(
this
.
get
(
'
file
'
))
{
this
.
on
(
'
change:put
'
,
this
.
uploadFile
,
this
);
}
this
.
setTimerForEphemeralMessage
();
/**
* Triggered once a {@link _converse.Message} has been created and initialized.
* @event _converse#messageInitialized
* @type { _converse.Message}
* @example _converse.api.listen.on('messageInitialized', model => { ... });
*/
await
api
.
trigger
(
'
messageInitialized
'
,
this
,
{
'
Synchronous
'
:
true
});
this
.
initialized
.
resolve
();
},
/**
* Sets an auto-destruct timer for this message, if it's is_ephemeral.
* @private
* @method _converse.Message#setTimerForEphemeralMessage
* @returns { Boolean } - Indicates whether the message is
* ephemeral or not, and therefore whether the timer was set or not.
*/
setTimerForEphemeralMessage
()
{
const
setTimer
=
()
=>
{
this
.
ephemeral_timer
=
window
.
setTimeout
(
this
.
safeDestroy
.
bind
(
this
),
10000
);
};
if
(
this
.
isEphemeral
())
{
setTimer
();
return
true
;
}
else
{
this
.
on
(
'
change:is_ephemeral
'
,
()
=>
this
.
isEphemeral
()
?
setTimer
()
:
clearTimeout
(
this
.
ephemeral_timer
)
);
return
false
;
}
},
checkValidity
()
{
if
(
Object
.
keys
(
this
.
attributes
).
length
===
3
)
{
// XXX: This is an empty message with only the 3 default values.
// This seems to happen when saving a newly created message
// fails for some reason.
// TODO: This is likely fixable by setting `wait` when
// creating messages. See the wait-for-messages branch.
this
.
validationError
=
'
Empty message
'
;
this
.
safeDestroy
();
return
false
;
}
return
true
;
},
/**
* Determines whether this messsage may be retracted by the current user.
* @private
* @method _converse.Messages#mayBeRetracted
* @returns { Boolean }
*/
mayBeRetracted
()
{
const
is_own_message
=
this
.
get
(
'
sender
'
)
===
'
me
'
;
return
is_own_message
&&
[
'
all
'
,
'
own
'
].
includes
(
api
.
settings
.
get
(
'
allow_message_retraction
'
));
},
safeDestroy
()
{
try
{
this
.
destroy
();
}
catch
(
e
)
{
log
.
error
(
e
);
}
},
isEphemeral
()
{
return
this
.
get
(
'
is_ephemeral
'
);
},
getDisplayName
()
{
if
(
this
.
get
(
'
type
'
)
===
'
groupchat
'
)
{
return
this
.
get
(
'
nick
'
);
}
else
if
(
this
.
contact
)
{
return
this
.
contact
.
getDisplayName
();
}
else
if
(
this
.
vcard
)
{
return
this
.
vcard
.
getDisplayName
();
}
else
{
return
this
.
get
(
'
from
'
);
}
},
getMessageText
()
{
const
{
__
}
=
_converse
;
if
(
this
.
get
(
'
is_encrypted
'
))
{
return
this
.
get
(
'
plaintext
'
)
||
this
.
get
(
'
body
'
)
||
__
(
'
Undecryptable OMEMO message
'
);
}
return
this
.
get
(
'
message
'
);
},
isMeCommand
()
{
const
text
=
this
.
getMessageText
();
if
(
!
text
)
{
return
false
;
}
return
text
.
startsWith
(
'
/me
'
);
},
/**
* Send out an IQ stanza to request a file upload slot.
* https://xmpp.org/extensions/xep-0363.html#request
* @private
* @method _converse.Message#sendSlotRequestStanza
*/
sendSlotRequestStanza
()
{
if
(
!
this
.
file
)
{
return
Promise
.
reject
(
new
Error
(
'
file is undefined
'
));
}
const
iq
=
converse
.
env
.
$iq
({
'
from
'
:
_converse
.
jid
,
'
to
'
:
this
.
get
(
'
slot_request_url
'
),
'
type
'
:
'
get
'
})
.
c
(
'
request
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HTTPUPLOAD
,
'
filename
'
:
this
.
file
.
name
,
'
size
'
:
this
.
file
.
size
,
'
content-type
'
:
this
.
file
.
type
});
return
api
.
sendIQ
(
iq
);
},
async
getRequestSlotURL
()
{
const
{
__
}
=
_converse
;
let
stanza
;
try
{
stanza
=
await
this
.
sendSlotRequestStanza
();
}
catch
(
e
)
{
log
.
error
(
e
);
return
this
.
save
({
'
type
'
:
'
error
'
,
'
message
'
:
__
(
'
Sorry, could not determine upload URL.
'
),
'
is_ephemeral
'
:
true
});
}
const
slot
=
stanza
.
querySelector
(
'
slot
'
);
if
(
slot
)
{
this
.
save
({
'
get
'
:
slot
.
querySelector
(
'
get
'
).
getAttribute
(
'
url
'
),
'
put
'
:
slot
.
querySelector
(
'
put
'
).
getAttribute
(
'
url
'
)
});
}
else
{
return
this
.
save
({
'
type
'
:
'
error
'
,
'
message
'
:
__
(
'
Sorry, could not determine file upload URL.
'
),
'
is_ephemeral
'
:
true
});
}
},
uploadFile
()
{
const
xhr
=
new
XMLHttpRequest
();
xhr
.
onreadystatechange
=
()
=>
{
if
(
xhr
.
readyState
===
XMLHttpRequest
.
DONE
)
{
log
.
info
(
'
Status:
'
+
xhr
.
status
);
if
(
xhr
.
status
===
200
||
xhr
.
status
===
201
)
{
this
.
save
({
'
upload
'
:
_converse
.
SUCCESS
,
'
oob_url
'
:
this
.
get
(
'
get
'
),
'
message
'
:
this
.
get
(
'
get
'
)
});
}
else
{
xhr
.
onerror
();
}
}
};
xhr
.
upload
.
addEventListener
(
'
progress
'
,
evt
=>
{
if
(
evt
.
lengthComputable
)
{
this
.
set
(
'
progress
'
,
evt
.
loaded
/
evt
.
total
);
}
},
false
);
xhr
.
onerror
=
()
=>
{
const
{
__
}
=
_converse
;
let
message
;
if
(
xhr
.
responseText
)
{
message
=
__
(
'
Sorry, could not succesfully upload your file. Your server’s response: "%1$s"
'
,
xhr
.
responseText
);
}
else
{
message
=
__
(
'
Sorry, could not succesfully upload your file.
'
);
}
this
.
save
({
'
type
'
:
'
error
'
,
'
upload
'
:
_converse
.
FAILURE
,
'
message
'
:
message
,
'
is_ephemeral
'
:
true
});
};
xhr
.
open
(
'
PUT
'
,
this
.
get
(
'
put
'
),
true
);
xhr
.
setRequestHeader
(
'
Content-type
'
,
this
.
file
.
type
);
xhr
.
send
(
this
.
file
);
}
};
export
default
MessageMixin
;
src/headless/plugins/chat/model-with-contact.js
0 → 100644
View file @
e3ebde97
import
{
converse
}
from
"
../../core.js
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
const
u
=
converse
.
env
.
utils
;
const
ModelWithContact
=
Model
.
extend
({
initialize
()
{
this
.
rosterContactAdded
=
u
.
getResolveablePromise
();
},
async
setRosterContact
(
jid
)
{
const
contact
=
await
api
.
contacts
.
get
(
jid
);
if
(
contact
)
{
this
.
contact
=
contact
;
this
.
set
(
'
nickname
'
,
contact
.
get
(
'
nickname
'
));
this
.
rosterContactAdded
.
resolve
();
}
}
});
export
default
ModelWithContact
;
src/headless/plugins/chat/model.js
0 → 100644
View file @
e3ebde97
import
ModelWithContact
from
'
./model-with-contact.js
'
;
import
filesize
from
"
filesize
"
;
import
log
from
"
../../log.js
"
;
import
st
from
"
../../utils/stanza
"
;
import
{
Model
}
from
'
@converse/skeletor/src/model.js
'
;
import
{
_converse
,
api
,
converse
}
from
"
../../core.js
"
;
import
{
find
,
isMatch
,
isObject
,
pick
}
from
"
lodash-es
"
;
const
{
Strophe
,
$msg
}
=
converse
.
env
;
const
u
=
converse
.
env
.
utils
;
/**
* Represents an open/ongoing chat conversation.
*
* @class
* @namespace _converse.ChatBox
* @memberOf _converse
*/
const
ChatBox
=
ModelWithContact
.
extend
({
defaults
()
{
return
{
'
bookmarked
'
:
false
,
'
chat_state
'
:
undefined
,
'
hidden
'
:
_converse
.
isUniView
()
&&
!
api
.
settings
.
get
(
'
singleton
'
),
'
message_type
'
:
'
chat
'
,
'
nickname
'
:
undefined
,
'
num_unread
'
:
0
,
'
time_sent
'
:
(
new
Date
(
0
)).
toISOString
(),
'
time_opened
'
:
this
.
get
(
'
time_opened
'
)
||
(
new
Date
()).
getTime
(),
'
type
'
:
_converse
.
PRIVATE_CHAT_TYPE
,
'
url
'
:
''
}
},
async
initialize
()
{
this
.
initialized
=
u
.
getResolveablePromise
();
ModelWithContact
.
prototype
.
initialize
.
apply
(
this
,
arguments
);
const
jid
=
this
.
get
(
'
jid
'
);
if
(
!
jid
)
{
// XXX: The `validate` method will prevent this model
// from being persisted if there's no jid, but that gets
// called after model instantiation, so we have to deal
// with invalid models here also.
// This happens when the controlbox is in browser storage,
// but we're in embedded mode.
return
;
}
this
.
set
({
'
box_id
'
:
`box-
${
jid
}
`
});
this
.
initNotifications
();
this
.
initMessages
();
if
(
this
.
get
(
'
type
'
)
===
_converse
.
PRIVATE_CHAT_TYPE
)
{
this
.
presence
=
_converse
.
presences
.
findWhere
({
'
jid
'
:
jid
})
||
_converse
.
presences
.
create
({
'
jid
'
:
jid
});
await
this
.
setRosterContact
(
jid
);
}
this
.
on
(
'
change:chat_state
'
,
this
.
sendChatState
,
this
);
await
this
.
fetchMessages
();
/**
* Triggered once a {@link _converse.ChatBox} has been created and initialized.
* @event _converse#chatBoxInitialized
* @type { _converse.ChatBox}
* @example _converse.api.listen.on('chatBoxInitialized', model => { ... });
*/
await
api
.
trigger
(
'
chatBoxInitialized
'
,
this
,
{
'
Synchronous
'
:
true
});
this
.
initialized
.
resolve
();
},
getMessagesCollection
()
{
return
new
_converse
.
Messages
();
},
getMessagesCacheKey
()
{
return
`converse.messages-
${
this
.
get
(
'
jid
'
)}
-
${
_converse
.
bare_jid
}
`
;
},
initMessages
()
{
this
.
messages
=
this
.
getMessagesCollection
();
this
.
messages
.
fetched
=
u
.
getResolveablePromise
();
this
.
messages
.
fetched
.
then
(()
=>
{
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBoxView | _converse.ChatRoomView}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
api
.
trigger
(
'
afterMessagesFetched
'
,
this
);
});
this
.
messages
.
chatbox
=
this
;
this
.
messages
.
browserStorage
=
_converse
.
createStore
(
this
.
getMessagesCacheKey
());
this
.
listenTo
(
this
.
messages
,
'
change:upload
'
,
message
=>
{
if
(
message
.
get
(
'
upload
'
)
===
_converse
.
SUCCESS
)
{
api
.
send
(
this
.
createMessageStanza
(
message
));
}
});
},
initNotifications
()
{
this
.
notifications
=
new
Model
();
},
afterMessagesFetched
()
{
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
api
.
trigger
(
'
afterMessagesFetched
'
,
this
);
},
fetchMessages
()
{
if
(
this
.
messages
.
fetched_flag
)
{
log
.
info
(
`Not re-fetching messages for
${
this
.
get
(
'
jid
'
)}
`
);
return
;
}
this
.
messages
.
fetched_flag
=
true
;
const
resolve
=
this
.
messages
.
fetched
.
resolve
;
this
.
messages
.
fetch
({
'
add
'
:
true
,
'
success
'
:
()
=>
{
this
.
afterMessagesFetched
();
resolve
()
},
'
error
'
:
()
=>
{
this
.
afterMessagesFetched
();
resolve
()
}
});
return
this
.
messages
.
fetched
;
},
async
handleErrorMessageStanza
(
stanza
)
{
const
{
__
}
=
_converse
;
const
attrs
=
await
st
.
parseMessage
(
stanza
,
_converse
);
if
(
!
await
this
.
shouldShowErrorMessage
(
attrs
))
{
return
;
}
const
message
=
this
.
getMessageReferencedByError
(
attrs
);
if
(
message
)
{
const
new_attrs
=
{
'
error
'
:
attrs
.
error
,
'
error_condition
'
:
attrs
.
error_condition
,
'
error_text
'
:
attrs
.
error_text
,
'
error_type
'
:
attrs
.
error_type
,
'
editable
'
:
false
,
};
if
(
attrs
.
msgid
===
message
.
get
(
'
retraction_id
'
))
{
// The error message refers to a retraction
new_attrs
.
retraction_id
=
undefined
;
if
(
!
attrs
.
error
)
{
if
(
attrs
.
error_condition
===
'
forbidden
'
)
{
new_attrs
.
error
=
__
(
"
You're not allowed to retract your message.
"
);
}
else
{
new_attrs
.
error
=
__
(
'
Sorry, an error occurred while trying to retract your message.
'
);
}
}
}
else
if
(
!
attrs
.
error
)
{
if
(
attrs
.
error_condition
===
'
forbidden
'
)
{
new_attrs
.
error
=
__
(
"
You're not allowed to send a message.
"
);
}
else
{
new_attrs
.
error
=
__
(
'
Sorry, an error occurred while trying to send your message.
'
);
}
}
message
.
save
(
new_attrs
);
}
else
{
this
.
createMessage
(
attrs
);
}
},
/**
* Queue an incoming `chat` message stanza for processing.
* @async
* @private
* @method _converse.ChatRoom#queueMessage
* @param { Promise<MessageAttributes> } attrs - A promise which resolves to the message attributes
*/
queueMessage
(
attrs
)
{
this
.
msg_chain
=
(
this
.
msg_chain
||
this
.
messages
.
fetched
)
.
then
(()
=>
this
.
onMessage
(
attrs
))
.
catch
(
e
=>
log
.
error
(
e
));
return
this
.
msg_chain
;
},
/**
* @async
* @private
* @method _converse.ChatRoom#onMessage
* @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes.
*/
async
onMessage
(
attrs
)
{
attrs
=
await
attrs
;
if
(
u
.
isErrorObject
(
attrs
))
{
attrs
.
stanza
&&
log
.
error
(
attrs
.
stanza
);
return
log
.
error
(
attrs
.
message
);
}
const
message
=
this
.
getDuplicateMessage
(
attrs
);
if
(
message
)
{
this
.
updateMessage
(
message
,
attrs
);
}
else
if
(
!
this
.
handleReceipt
(
attrs
)
&&
!
this
.
handleChatMarker
(
attrs
)
&&
!
(
await
this
.
handleRetraction
(
attrs
))
)
{
this
.
setEditable
(
attrs
,
attrs
.
time
);
if
(
attrs
[
'
chat_state
'
]
&&
attrs
.
sender
===
'
them
'
)
{
this
.
notifications
.
set
(
'
chat_state
'
,
attrs
.
chat_state
);
}
if
(
u
.
shouldCreateMessage
(
attrs
))
{
const
msg
=
this
.
handleCorrection
(
attrs
)
||
await
this
.
createMessage
(
attrs
);
this
.
notifications
.
set
({
'
chat_state
'
:
null
});
this
.
handleUnreadMessage
(
msg
);
}
}
},
async
clearMessages
()
{
try
{
await
this
.
messages
.
clearStore
();
}
catch
(
e
)
{
this
.
messages
.
trigger
(
'
reset
'
);
log
.
error
(
e
);
}
finally
{
delete
this
.
msg_chain
;
delete
this
.
messages
.
fetched_flag
;
this
.
messages
.
fetched
=
u
.
getResolveablePromise
();
}
},
async
close
()
{
try
{
await
new
Promise
((
success
,
reject
)
=>
{
return
this
.
destroy
({
success
,
'
error
'
:
(
m
,
e
)
=>
reject
(
e
)})
});
}
catch
(
e
)
{
log
.
error
(
e
);
}
finally
{
if
(
api
.
settings
.
get
(
'
clear_messages_on_reconnection
'
))
{
await
this
.
clearMessages
();
}
}
},
announceReconnection
()
{
/**
* Triggered whenever a `_converse.ChatBox` instance has reconnected after an outage
* @event _converse#onChatReconnected
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('onChatReconnected', chatbox => { ... });
*/
api
.
trigger
(
'
chatReconnected
'
,
this
);
},
async
onReconnection
()
{
if
(
api
.
settings
.
get
(
'
clear_messages_on_reconnection
'
))
{
await
this
.
clearMessages
();
}
this
.
announceReconnection
();
},
validate
(
attrs
)
{
if
(
!
attrs
.
jid
)
{
return
'
Ignored ChatBox without JID
'
;
}
const
room_jids
=
_converse
.
auto_join_rooms
.
map
(
s
=>
isObject
(
s
)
?
s
.
jid
:
s
);
const
auto_join
=
api
.
settings
.
get
(
'
auto_join_private_chats
'
).
concat
(
room_jids
);
if
(
api
.
settings
.
get
(
"
singleton
"
)
&&
!
auto_join
.
includes
(
attrs
.
jid
)
&&
!
api
.
settings
.
get
(
'
auto_join_on_invite
'
))
{
const
msg
=
`
${
attrs
.
jid
}
is not allowed because singleton is true and it's not being auto_joined`
;
log
.
warn
(
msg
);
return
msg
;
}
},
getDisplayName
()
{
if
(
this
.
contact
)
{
return
this
.
contact
.
getDisplayName
();
}
else
if
(
this
.
vcard
)
{
return
this
.
vcard
.
getDisplayName
();
}
else
{
return
this
.
get
(
'
jid
'
);
}
},
async
createMessageFromError
(
error
)
{
if
(
error
instanceof
_converse
.
TimeoutError
)
{
const
msg
=
await
this
.
createMessage
({
'
type
'
:
'
error
'
,
'
message
'
:
error
.
message
,
'
retry_event_id
'
:
error
.
retry_event_id
});
msg
.
error
=
error
;
}
},
getOldestMessage
()
{
for
(
let
i
=
0
;
i
<
this
.
messages
.
length
;
i
++
)
{
const
message
=
this
.
messages
.
at
(
i
);
if
(
message
.
get
(
'
type
'
)
===
this
.
get
(
'
message_type
'
))
{
return
message
;
}
}
},
getMostRecentMessage
()
{
for
(
let
i
=
this
.
messages
.
length
-
1
;
i
>=
0
;
i
--
)
{
const
message
=
this
.
messages
.
at
(
i
);
if
(
message
.
get
(
'
type
'
)
===
this
.
get
(
'
message_type
'
))
{
return
message
;
}
}
},
getUpdatedMessageAttributes
(
message
,
attrs
)
{
// Filter the attrs object, restricting it to only the `is_archived` key.
return
(({
is_archived
})
=>
({
is_archived
}))(
attrs
)
},
updateMessage
(
message
,
attrs
)
{
const
new_attrs
=
this
.
getUpdatedMessageAttributes
(
message
,
attrs
);
new_attrs
&&
message
.
save
(
new_attrs
);
},
/**
* Mutator for setting the chat state of this chat session.
* Handles clearing of any chat state notification timeouts and
* setting new ones if necessary.
* Timeouts are set when the state being set is COMPOSING or PAUSED.
* After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
* See XEP-0085 Chat State Notifications.
* @private
* @method _converse.ChatBox#setChatState
* @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
*/
setChatState
(
state
,
options
)
{
if
(
this
.
chat_state_timeout
!==
undefined
)
{
window
.
clearTimeout
(
this
.
chat_state_timeout
);
delete
this
.
chat_state_timeout
;
}
if
(
state
===
_converse
.
COMPOSING
)
{
this
.
chat_state_timeout
=
window
.
setTimeout
(
this
.
setChatState
.
bind
(
this
),
_converse
.
TIMEOUTS
.
PAUSED
,
_converse
.
PAUSED
);
}
else
if
(
state
===
_converse
.
PAUSED
)
{
this
.
chat_state_timeout
=
window
.
setTimeout
(
this
.
setChatState
.
bind
(
this
),
_converse
.
TIMEOUTS
.
INACTIVE
,
_converse
.
INACTIVE
);
}
this
.
set
(
'
chat_state
'
,
state
,
options
);
return
this
;
},
/**
* Given an error `<message>` stanza's attributes, find the saved message model which is
* referenced by that error.
* @param { Object } attrs
*/
getMessageReferencedByError
(
attrs
)
{
const
id
=
attrs
.
msgid
;
return
id
&&
this
.
messages
.
models
.
find
(
m
=>
[
m
.
get
(
'
msgid
'
),
m
.
get
(
'
retraction_id
'
)].
includes
(
id
));
},
/**
* @private
* @method _converse.ChatBox#shouldShowErrorMessage
* @returns {boolean}
*/
shouldShowErrorMessage
(
attrs
)
{
const
msg
=
this
.
getMessageReferencedByError
(
attrs
);
if
(
!
msg
&&
!
attrs
.
body
)
{
// If the error refers to a message not included in our store,
// and it doesn't have a <body> tag, we assume that this was a
// CSI message (which we don't store).
// See https://github.com/conversejs/converse.js/issues/1317
return
;
}
// Gets overridden in ChatRoom
return
true
;
},
isSameUser
(
jid1
,
jid2
)
{
return
u
.
isSameBareJID
(
jid1
,
jid2
);
},
/**
* Looks whether we already have a retraction for this
* incoming message. If so, it's considered "dangling" because it
* probably hasn't been applied to anything yet, given that the
* relevant message is only coming in now.
* @private
* @method _converse.ChatBox#findDanglingRetraction
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns { _converse.Message }
*/
findDanglingRetraction
(
attrs
)
{
if
(
!
attrs
.
origin_id
||
!
this
.
messages
.
length
)
{
return
null
;
}
// Only look for dangling retractions if there are newer
// messages than this one, since retractions come after.
if
(
this
.
messages
.
last
().
get
(
'
time
'
)
>
attrs
.
time
)
{
// Search from latest backwards
const
messages
=
Array
.
from
(
this
.
messages
.
models
);
messages
.
reverse
();
return
messages
.
find
(
({
attributes
})
=>
attributes
.
retracted_id
===
attrs
.
origin_id
&&
attributes
.
from
===
attrs
.
from
&&
!
attributes
.
moderated_by
);
}
},
/**
* Handles message retraction based on the passed in attributes.
* @private
* @method _converse.ChatBox#handleRetraction
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns { Boolean } Returns `true` or `false` depending on
* whether a message was retracted or not.
*/
async
handleRetraction
(
attrs
)
{
const
RETRACTION_ATTRIBUTES
=
[
'
retracted
'
,
'
retracted_id
'
,
'
editable
'
];
if
(
attrs
.
retracted
)
{
if
(
attrs
.
is_tombstone
)
{
return
false
;
}
const
message
=
this
.
messages
.
findWhere
({
'
origin_id
'
:
attrs
.
retracted_id
,
'
from
'
:
attrs
.
from
});
if
(
!
message
)
{
attrs
[
'
dangling_retraction
'
]
=
true
;
await
this
.
createMessage
(
attrs
);
return
true
;
}
message
.
save
(
pick
(
attrs
,
RETRACTION_ATTRIBUTES
));
return
true
;
}
else
{
// Check if we have dangling retraction
const
message
=
this
.
findDanglingRetraction
(
attrs
);
if
(
message
)
{
const
retraction_attrs
=
pick
(
message
.
attributes
,
RETRACTION_ATTRIBUTES
);
const
new_attrs
=
Object
.
assign
({
'
dangling_retraction
'
:
false
},
attrs
,
retraction_attrs
);
delete
new_attrs
[
'
id
'
];
// Delete id, otherwise a new cache entry gets created
message
.
save
(
new_attrs
);
return
true
;
}
}
return
false
;
},
/**
* Determines whether the passed in message attributes represent a
* message which corrects a previously received message, or an
* older message which has already been corrected.
* In both cases, update the corrected message accordingly.
* @private
* @method _converse.ChatBox#handleCorrection
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns { _converse.Message|undefined } Returns the corrected
* message or `undefined` if not applicable.
*/
handleCorrection
(
attrs
)
{
if
(
!
attrs
.
replace_id
||
!
attrs
.
from
)
{
return
;
}
const
message
=
this
.
messages
.
findWhere
({
'
msgid
'
:
attrs
.
replace_id
,
'
from
'
:
attrs
.
from
});
if
(
!
message
)
{
return
;
}
const
older_versions
=
message
.
get
(
'
older_versions
'
)
||
{};
if
((
attrs
.
time
<
message
.
get
(
'
time
'
))
&&
message
.
get
(
'
edited
'
))
{
// This is an older message which has been corrected afterwards
older_versions
[
attrs
.
time
]
=
attrs
[
'
message
'
];
message
.
save
({
'
older_versions
'
:
older_versions
});
}
else
{
// This is a correction of an earlier message we already received
if
(
Object
.
keys
(
older_versions
).
length
)
{
older_versions
[
message
.
get
(
'
edited
'
)]
=
message
.
get
(
'
message
'
);
}
else
{
older_versions
[
message
.
get
(
'
time
'
)]
=
message
.
get
(
'
message
'
);
}
attrs
=
Object
.
assign
(
attrs
,
{
'
older_versions
'
:
older_versions
});
delete
attrs
[
'
id
'
];
// Delete id, otherwise a new cache entry gets created
attrs
[
'
time
'
]
=
message
.
get
(
'
time
'
);
message
.
save
(
attrs
);
}
return
message
;
},
/**
* Returns an already cached message (if it exists) based on the
* passed in attributes map.
* @private
* @method _converse.ChatBox#getDuplicateMessage
* @param { object } attrs - Attributes representing a received
* message, as returned by {@link st.parseMessage}
* @returns {Promise<_converse.Message>}
*/
getDuplicateMessage
(
attrs
)
{
const
queries
=
[
...
this
.
getStanzaIdQueryAttrs
(
attrs
),
this
.
getOriginIdQueryAttrs
(
attrs
),
this
.
getMessageBodyQueryAttrs
(
attrs
)
].
filter
(
s
=>
s
);
const
msgs
=
this
.
messages
.
models
;
return
find
(
msgs
,
m
=>
queries
.
reduce
((
out
,
q
)
=>
(
out
||
isMatch
(
m
.
attributes
,
q
)),
false
));
},
getOriginIdQueryAttrs
(
attrs
)
{
return
attrs
.
origin_id
&&
{
'
origin_id
'
:
attrs
.
origin_id
,
'
from
'
:
attrs
.
from
};
},
getStanzaIdQueryAttrs
(
attrs
)
{
const
keys
=
Object
.
keys
(
attrs
).
filter
(
k
=>
k
.
startsWith
(
'
stanza_id
'
));
return
keys
.
map
(
key
=>
{
const
by_jid
=
key
.
replace
(
/^stanza_id /
,
''
);
const
query
=
{};
query
[
`stanza_id
${
by_jid
}
`
]
=
attrs
[
key
];
return
query
;
});
},
getMessageBodyQueryAttrs
(
attrs
)
{
if
(
attrs
.
message
&&
attrs
.
msgid
)
{
const
query
=
{
'
from
'
:
attrs
.
from
,
'
msgid
'
:
attrs
.
msgid
}
if
(
!
attrs
.
is_encrypted
)
{
// We can't match the message if it's a reflected
// encrypted message (e.g. via MAM or in a MUC)
query
[
'
message
'
]
=
attrs
.
message
;
}
return
query
;
}
},
/**
* Retract one of your messages in this chat
* @private
* @method _converse.ChatBoxView#retractOwnMessage
* @param { _converse.Message } message - The message which we're retracting.
*/
retractOwnMessage
(
message
)
{
this
.
sendRetractionMessage
(
message
)
message
.
save
({
'
retracted
'
:
(
new
Date
()).
toISOString
(),
'
retracted_id
'
:
message
.
get
(
'
origin_id
'
),
'
retraction_id
'
:
message
.
get
(
'
id
'
),
'
is_ephemeral
'
:
true
,
'
editable
'
:
false
});
},
/**
* Sends a message stanza to retract a message in this chat
* @private
* @method _converse.ChatBox#sendRetractionMessage
* @param { _converse.Message } message - The message which we're retracting.
*/
sendRetractionMessage
(
message
)
{
const
origin_id
=
message
.
get
(
'
origin_id
'
);
if
(
!
origin_id
)
{
throw
new
Error
(
"
Can't retract message without a XEP-0359 Origin ID
"
);
}
const
msg
=
$msg
({
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
"
chat
"
})
.
c
(
'
store
'
,
{
xmlns
:
Strophe
.
NS
.
HINTS
}).
up
()
.
c
(
"
apply-to
"
,
{
'
id
'
:
origin_id
,
'
xmlns
'
:
Strophe
.
NS
.
FASTEN
}).
c
(
'
retract
'
,
{
xmlns
:
Strophe
.
NS
.
RETRACT
})
return
_converse
.
connection
.
send
(
msg
);
},
sendMarkerForMessage
(
msg
)
{
if
(
msg
?.
get
(
'
is_markable
'
))
{
const
from_jid
=
Strophe
.
getBareJidFromJid
(
msg
.
get
(
'
from
'
));
this
.
sendMarker
(
from_jid
,
msg
.
get
(
'
msgid
'
),
'
displayed
'
,
msg
.
get
(
'
type
'
));
}
},
sendMarker
(
to_jid
,
id
,
type
,
msg_type
)
{
const
stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
to_jid
,
'
type
'
:
msg_type
?
msg_type
:
'
chat
'
}).
c
(
type
,
{
'
xmlns
'
:
Strophe
.
NS
.
MARKERS
,
'
id
'
:
id
});
api
.
send
(
stanza
);
},
handleChatMarker
(
attrs
)
{
const
to_bare_jid
=
Strophe
.
getBareJidFromJid
(
attrs
.
to
);
if
(
to_bare_jid
!==
_converse
.
bare_jid
)
{
return
false
;
}
if
(
attrs
.
is_markable
)
{
if
(
this
.
contact
&&
!
attrs
.
is_archived
&&
!
attrs
.
is_carbon
)
{
this
.
sendMarker
(
attrs
.
from
,
attrs
.
msgid
,
'
received
'
);
}
return
false
;
}
else
if
(
attrs
.
marker_id
)
{
const
message
=
this
.
messages
.
findWhere
({
'
msgid
'
:
attrs
.
marker_id
});
const
field_name
=
`marker_
${
attrs
.
marker
}
`
;
if
(
message
&&
!
message
.
get
(
field_name
))
{
message
.
save
({
field_name
:
(
new
Date
()).
toISOString
()});
}
return
true
;
}
},
sendReceiptStanza
(
to_jid
,
id
)
{
const
receipt_stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
to_jid
,
'
type
'
:
'
chat
'
,
}).
c
(
'
received
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
RECEIPTS
,
'
id
'
:
id
}).
up
()
.
c
(
'
store
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HINTS
}).
up
();
api
.
send
(
receipt_stanza
);
},
handleReceipt
(
attrs
)
{
if
(
attrs
.
sender
===
'
them
'
)
{
if
(
attrs
.
is_valid_receipt_request
)
{
this
.
sendReceiptStanza
(
attrs
.
from
,
attrs
.
msgid
);
}
else
if
(
attrs
.
receipt_id
)
{
const
message
=
this
.
messages
.
findWhere
({
'
msgid
'
:
attrs
.
receipt_id
});
if
(
message
&&
!
message
.
get
(
'
received
'
))
{
message
.
save
({
'
received
'
:
(
new
Date
()).
toISOString
()});
}
return
true
;
}
}
return
false
;
},
/**
* Given a {@link _converse.Message} return the XML stanza that represents it.
* @private
* @method _converse.ChatBox#createMessageStanza
* @param { _converse.Message } message - The message object
*/
createMessageStanza
(
message
)
{
const
stanza
=
$msg
({
'
from
'
:
_converse
.
connection
.
jid
,
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
this
.
get
(
'
message_type
'
),
'
id
'
:
message
.
get
(
'
edited
'
)
&&
u
.
getUniqueId
()
||
message
.
get
(
'
msgid
'
),
}).
c
(
'
body
'
).
t
(
message
.
get
(
'
message
'
)).
up
()
.
c
(
_converse
.
ACTIVE
,
{
'
xmlns
'
:
Strophe
.
NS
.
CHATSTATES
}).
root
();
if
(
message
.
get
(
'
type
'
)
===
'
chat
'
)
{
stanza
.
c
(
'
request
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
RECEIPTS
}).
root
();
}
if
(
message
.
get
(
'
is_spoiler
'
))
{
if
(
message
.
get
(
'
spoiler_hint
'
))
{
stanza
.
c
(
'
spoiler
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
SPOILER
},
message
.
get
(
'
spoiler_hint
'
)).
root
();
}
else
{
stanza
.
c
(
'
spoiler
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
SPOILER
}).
root
();
}
}
(
message
.
get
(
'
references
'
)
||
[]).
forEach
(
reference
=>
{
const
attrs
=
{
'
xmlns
'
:
Strophe
.
NS
.
REFERENCE
,
'
begin
'
:
reference
.
begin
,
'
end
'
:
reference
.
end
,
'
type
'
:
reference
.
type
,
}
if
(
reference
.
uri
)
{
attrs
.
uri
=
reference
.
uri
;
}
stanza
.
c
(
'
reference
'
,
attrs
).
root
();
});
if
(
message
.
get
(
'
oob_url
'
))
{
stanza
.
c
(
'
x
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
OUTOFBAND
}).
c
(
'
url
'
).
t
(
message
.
get
(
'
oob_url
'
)).
root
();
}
if
(
message
.
get
(
'
edited
'
))
{
stanza
.
c
(
'
replace
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
MESSAGE_CORRECT
,
'
id
'
:
message
.
get
(
'
msgid
'
)
}).
root
();
}
if
(
message
.
get
(
'
origin_id
'
))
{
stanza
.
c
(
'
origin-id
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
SID
,
'
id
'
:
message
.
get
(
'
origin_id
'
)}).
root
();
}
return
stanza
;
},
getOutgoingMessageAttributes
(
text
,
spoiler_hint
)
{
const
is_spoiler
=
this
.
get
(
'
composing_spoiler
'
);
const
origin_id
=
u
.
getUniqueId
();
const
body
=
text
?
u
.
httpToGeoUri
(
u
.
shortnamesToUnicode
(
text
),
_converse
)
:
undefined
;
return
{
'
from
'
:
_converse
.
bare_jid
,
'
fullname
'
:
_converse
.
xmppstatus
.
get
(
'
fullname
'
),
'
id
'
:
origin_id
,
'
is_only_emojis
'
:
text
?
u
.
isOnlyEmojis
(
text
)
:
false
,
'
jid
'
:
this
.
get
(
'
jid
'
),
'
message
'
:
body
,
'
msgid
'
:
origin_id
,
'
nickname
'
:
this
.
get
(
'
nickname
'
),
'
sender
'
:
'
me
'
,
'
spoiler_hint
'
:
is_spoiler
?
spoiler_hint
:
undefined
,
'
time
'
:
(
new
Date
()).
toISOString
(),
'
type
'
:
this
.
get
(
'
message_type
'
),
body
,
is_spoiler
,
origin_id
}
},
/**
* Responsible for setting the editable attribute of messages.
* If api.settings.get('allow_message_corrections') is "last", then only the last
* message sent from me will be editable. If set to "all" all messages
* will be editable. Otherwise no messages will be editable.
* @method _converse.ChatBox#setEditable
* @memberOf _converse.ChatBox
* @param { Object } attrs An object containing message attributes.
* @param { String } send_time - time when the message was sent
*/
setEditable
(
attrs
,
send_time
)
{
if
(
attrs
.
is_headline
||
u
.
isEmptyMessage
(
attrs
)
||
attrs
.
sender
!==
'
me
'
)
{
return
;
}
if
(
api
.
settings
.
get
(
'
allow_message_corrections
'
)
===
'
all
'
)
{
attrs
.
editable
=
!
(
attrs
.
file
||
attrs
.
retracted
||
'
oob_url
'
in
attrs
);
}
else
if
((
api
.
settings
.
get
(
'
allow_message_corrections
'
)
===
'
last
'
)
&&
(
send_time
>
this
.
get
(
'
time_sent
'
)))
{
this
.
set
({
'
time_sent
'
:
send_time
});
const
msg
=
this
.
messages
.
findWhere
({
'
editable
'
:
true
});
if
(
msg
)
{
msg
.
save
({
'
editable
'
:
false
});
}
attrs
.
editable
=
!
(
attrs
.
file
||
attrs
.
retracted
||
'
oob_url
'
in
attrs
);
}
},
/**
* Queue the creation of a message, to make sure that we don't run
* into a race condition whereby we're creating a new message
* before the collection has been fetched.
* @async
* @private
* @method _converse.ChatRoom#queueMessageCreation
* @param { Object } attrs
*/
async
createMessage
(
attrs
,
options
)
{
attrs
.
time
=
attrs
.
time
||
(
new
Date
()).
toISOString
();
await
this
.
messages
.
fetched
;
const
p
=
this
.
messages
.
create
(
attrs
,
Object
.
assign
({
'
wait
'
:
true
,
'
promise
'
:
true
},
options
));
return
p
;
},
/**
* Responsible for sending off a text message inside an ongoing chat conversation.
* @private
* @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox
* @param { String } text - The chat message text
* @param { String } spoiler_hint - An optional hint, if the message being sent is a spoiler
* @returns { _converse.Message }
* @example
* const chat = api.chats.get('buddy1@example.com');
* chat.sendMessage('hello world');
*/
async
sendMessage
(
text
,
spoiler_hint
)
{
const
attrs
=
this
.
getOutgoingMessageAttributes
(
text
,
spoiler_hint
);
let
message
=
this
.
messages
.
findWhere
(
'
correcting
'
)
if
(
message
)
{
const
older_versions
=
message
.
get
(
'
older_versions
'
)
||
{};
older_versions
[
message
.
get
(
'
time
'
)]
=
message
.
get
(
'
message
'
);
message
.
save
({
'
correcting
'
:
false
,
'
edited
'
:
(
new
Date
()).
toISOString
(),
'
message
'
:
attrs
.
message
,
'
older_versions
'
:
older_versions
,
'
references
'
:
attrs
.
references
,
'
is_only_emojis
'
:
attrs
.
is_only_emojis
,
'
origin_id
'
:
u
.
getUniqueId
(),
'
received
'
:
undefined
});
}
else
{
this
.
setEditable
(
attrs
,
(
new
Date
()).
toISOString
());
message
=
await
this
.
createMessage
(
attrs
);
}
api
.
send
(
this
.
createMessageStanza
(
message
));
/**
* Triggered when a message is being sent out
* @event _converse#sendMessage
* @type { Object }
* @param { Object } data
* @property { (_converse.ChatBox | _converse.ChatRoom) } data.chatbox
* @property { (_converse.Message | _converse.ChatRoomMessage) } data.message
*/
api
.
trigger
(
'
sendMessage
'
,
{
'
chatbox
'
:
this
,
message
});
return
message
;
},
/**
* Sends a message with the current XEP-0085 chat state of the user
* as taken from the `chat_state` attribute of the {@link _converse.ChatBox}.
* @private
* @method _converse.ChatBox#sendChatState
*/
sendChatState
()
{
if
(
api
.
settings
.
get
(
'
send_chat_state_notifications
'
)
&&
this
.
get
(
'
chat_state
'
))
{
const
allowed
=
api
.
settings
.
get
(
'
send_chat_state_notifications
'
);
if
(
Array
.
isArray
(
allowed
)
&&
!
allowed
.
includes
(
this
.
get
(
'
chat_state
'
)))
{
return
;
}
api
.
send
(
$msg
({
'
id
'
:
u
.
getUniqueId
(),
'
to
'
:
this
.
get
(
'
jid
'
),
'
type
'
:
'
chat
'
}).
c
(
this
.
get
(
'
chat_state
'
),
{
'
xmlns
'
:
Strophe
.
NS
.
CHATSTATES
}).
up
()
.
c
(
'
no-store
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HINTS
}).
up
()
.
c
(
'
no-permanent-store
'
,
{
'
xmlns
'
:
Strophe
.
NS
.
HINTS
})
);
}
},
async
sendFiles
(
files
)
{
const
{
__
}
=
_converse
;
const
result
=
await
api
.
disco
.
features
.
get
(
Strophe
.
NS
.
HTTPUPLOAD
,
_converse
.
domain
);
const
item
=
result
.
pop
();
if
(
!
item
)
{
this
.
createMessage
({
'
message
'
:
__
(
"
Sorry, looks like file upload is not supported by your server.
"
),
'
type
'
:
'
error
'
,
'
is_ephemeral
'
:
true
});
return
;
}
const
data
=
item
.
dataforms
.
where
({
'
FORM_TYPE
'
:
{
'
value
'
:
Strophe
.
NS
.
HTTPUPLOAD
,
'
type
'
:
"
hidden
"
}}).
pop
();
const
max_file_size
=
window
.
parseInt
((
data
?.
attributes
||
{})[
'
max-file-size
'
]?.
value
);
const
slot_request_url
=
item
?.
id
;
if
(
!
slot_request_url
)
{
this
.
createMessage
({
'
message
'
:
__
(
"
Sorry, looks like file upload is not supported by your server.
"
),
'
type
'
:
'
error
'
,
'
is_ephemeral
'
:
true
});
return
;
}
Array
.
from
(
files
).
forEach
(
async
file
=>
{
if
(
!
window
.
isNaN
(
max_file_size
)
&&
window
.
parseInt
(
file
.
size
)
>
max_file_size
)
{
return
this
.
createMessage
({
'
message
'
:
__
(
'
The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.
'
,
file
.
name
,
filesize
(
max_file_size
)),
'
type
'
:
'
error
'
,
'
is_ephemeral
'
:
true
});
}
else
{
const
attrs
=
Object
.
assign
(
this
.
getOutgoingMessageAttributes
(),
{
'
file
'
:
true
,
'
progress
'
:
0
,
'
slot_request_url
'
:
slot_request_url
});
this
.
setEditable
(
attrs
,
(
new
Date
()).
toISOString
());
const
message
=
await
this
.
createMessage
(
attrs
,
{
'
silent
'
:
true
});
message
.
file
=
file
;
this
.
messages
.
trigger
(
'
add
'
,
message
);
message
.
getRequestSlotURL
();
}
});
},
maybeShow
(
force
)
{
if
(
force
)
{
if
(
_converse
.
isUniView
())
{
// We only have one chat visible at any one time.
// So before opening a chat, we make sure all other chats are hidden.
const
filter
=
c
=>
!
c
.
get
(
'
hidden
'
)
&&
c
.
get
(
'
jid
'
)
!==
this
.
get
(
'
jid
'
)
&&
c
.
get
(
'
id
'
)
!==
'
controlbox
'
;
_converse
.
chatboxes
.
filter
(
filter
).
forEach
(
c
=>
u
.
safeSave
(
c
,
{
'
hidden
'
:
true
}));
}
u
.
safeSave
(
this
,
{
'
hidden
'
:
false
});
}
if
(
_converse
.
isUniView
()
&&
this
.
get
(
'
hidden
'
))
{
return
;
}
else
{
return
this
.
trigger
(
"
show
"
);
}
},
/**
* Indicates whether the chat is hidden and therefore
* whether a newly received message will be visible
* to the user or not.
* @returns {boolean}
*/
isHidden
()
{
// Note: This methods gets overridden by converse-minimize
const
hidden
=
_converse
.
isUniView
()
&&
this
.
get
(
'
hidden
'
);
return
hidden
||
this
.
isScrolledUp
()
||
_converse
.
windowState
===
'
hidden
'
;
},
/**
* Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary.
* @private
* @param {_converse.Message} message
*/
handleUnreadMessage
(
message
)
{
if
(
!
message
?.
get
(
'
body
'
))
{
return
}
if
(
u
.
isNewMessage
(
message
))
{
if
(
this
.
isHidden
())
{
const
settings
=
{
'
num_unread
'
:
this
.
get
(
'
num_unread
'
)
+
1
};
if
(
this
.
get
(
'
num_unread
'
)
===
0
)
{
settings
[
'
first_unread_id
'
]
=
message
.
get
(
'
id
'
);
}
this
.
save
(
settings
);
}
else
{
this
.
sendMarkerForMessage
(
message
);
}
}
},
clearUnreadMsgCounter
()
{
if
(
this
.
get
(
'
num_unread
'
)
>
0
)
{
this
.
sendMarkerForMessage
(
this
.
messages
.
last
());
}
u
.
safeSave
(
this
,
{
'
num_unread
'
:
0
});
},
isScrolledUp
()
{
return
this
.
get
(
'
scrolled
'
,
true
);
}
});
export
default
ChatBox
;
src/headless/plugins/muc.js
View file @
e3ebde97
...
...
@@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2)
* @description Implements the non-view logic for XEP-0045 Multi-User Chat
*/
import
"
./chat
"
;
import
"
./chat
/index.js
"
;
import
"
./disco
"
;
import
"
./emoji/index.js
"
;
import
{
Collection
}
from
"
@converse/skeletor/src/collection
"
;
...
...
@@ -398,7 +398,6 @@ converse.plugins.add('converse-muc', {
* @memberOf _converse
*/
_converse
.
ChatRoom
=
_converse
.
ChatBox
.
extend
({
messagesCollection
:
_converse
.
ChatRoomMessages
,
defaults
()
{
return
{
...
...
@@ -595,6 +594,10 @@ converse.plugins.add('converse-muc', {
this
.
announceReconnection
();
},
getMessagesCollection
()
{
return
new
_converse
.
ChatRoomMessages
();
},
restoreSession
()
{
const
id
=
`muc.session-
${
_converse
.
bare_jid
}
-
${
this
.
get
(
'
jid
'
)}
`
;
this
.
session
=
new
MUCSession
({
id
});
...
...
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