Repository: fsharechat/vue-chat
Branch: master
Commit: 948ffd3a4082
Files: 146
Total size: 414.9 KB
Directory structure:
gitextract_p2lxtd1d/
├── .babelrc
├── .editorconfig
├── .gitignore
├── .postcssrc.js
├── LICENSE
├── README.md
├── build/
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config/
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── index.html
├── index.md
├── package.json
├── src/
│ ├── App.vue
│ ├── assets/
│ │ └── fonts/
│ │ ├── iconfont.css
│ │ ├── iconfont.js
│ │ └── iconfont.json
│ ├── components/
│ │ ├── chatlist/
│ │ │ └── chatlist.vue
│ │ ├── friendlist/
│ │ │ └── friendlist.vue
│ │ ├── info/
│ │ │ └── info.vue
│ │ ├── menu/
│ │ │ ├── addtip.vue
│ │ │ ├── groupInfo.vue
│ │ │ ├── personalCard.vue
│ │ │ ├── relayMessage.vue
│ │ │ └── rightMenu.vue
│ │ ├── message/
│ │ │ └── message.vue
│ │ ├── mycard/
│ │ │ └── mycard.vue
│ │ ├── search/
│ │ │ └── search.vue
│ │ └── text/
│ │ └── text.vue
│ ├── constant/
│ │ └── index.js
│ ├── main.js
│ ├── page/
│ │ ├── chat/
│ │ │ └── chat.vue
│ │ ├── friend/
│ │ │ ├── friend.vue
│ │ │ └── searchfriend.vue
│ │ ├── group/
│ │ │ ├── creategroup.vue
│ │ │ └── groupVideoCall.vue
│ │ ├── login/
│ │ │ └── login.vue
│ │ └── main.vue
│ ├── permission.js
│ ├── router/
│ │ └── index.js
│ ├── store.js
│ ├── webrtc/
│ │ ├── callEndReason.js
│ │ ├── callSession.js
│ │ ├── callState.js
│ │ ├── engineCallback.js
│ │ ├── groupCallClient.js
│ │ ├── message/
│ │ │ ├── callAnswerMessageContent.js
│ │ │ ├── callAnswerTMessageContent.js
│ │ │ ├── callByeMessageContent.js
│ │ │ ├── callModifyMessageContent.js
│ │ │ ├── callSignalMessageContent.js
│ │ │ └── callStartMessageContent.js
│ │ ├── participant.js
│ │ ├── sessionCallback.js
│ │ └── voipclient.js
│ └── websocket/
│ ├── chatManager.js
│ ├── future/
│ │ ├── futureResult.js
│ │ └── promiseResolve.js
│ ├── handler/
│ │ ├── abstractmessagehandler.js
│ │ ├── addGroupMemberHandler.js
│ │ ├── connectackhandler.js
│ │ ├── createGroupHandler.js
│ │ ├── dismissGroupHandler.js
│ │ ├── friendAddRequestHandler.js
│ │ ├── friendRequestHandler.js
│ │ ├── getGroupInfoHandler.js
│ │ ├── getGroupMemberHandler.js
│ │ ├── getMinioUploadUrlHandler.js
│ │ ├── getUploadtokenHandler.js
│ │ ├── getfriendresultHandler.js
│ │ ├── getuserinfoHandler.js
│ │ ├── handleFriendRequestHandler.js
│ │ ├── kickGroupmemberHandler.js
│ │ ├── loadRemoteMessageHander.js
│ │ ├── messageHandler.js
│ │ ├── modifyMyInfoHandler.js
│ │ ├── notifyFriendHandler.js
│ │ ├── notifyFriendRequestHandler.js
│ │ ├── notifyMessageHandler.js
│ │ ├── notifyRecallMessageHandler.js
│ │ ├── quitGroupHandler.js
│ │ ├── recallMessageHandler.js
│ │ ├── receiveMessageHandler.js
│ │ ├── searchUserResultHandler.js
│ │ ├── sendMessageHandler.js
│ │ └── setFriendAliasRequestHandler.js
│ ├── index.js
│ ├── listener/
│ │ └── onReceiverMessageListener.js
│ ├── message/
│ │ ├── fileMessageContent.js
│ │ ├── imageMessageContent.js
│ │ ├── mediaMessageContent.js
│ │ ├── message.js
│ │ ├── messageConfig.js
│ │ ├── messageContent.js
│ │ ├── messageContentMediaType.js
│ │ ├── messageContentType.js
│ │ ├── messagePayload.js
│ │ ├── messageStatus.js
│ │ ├── modifyGroupInfoType.js
│ │ ├── myInfoType.js
│ │ ├── notification/
│ │ │ ├── addGroupMemberNotification.js
│ │ │ ├── changeGroupNameNotification.js
│ │ │ ├── createGroupNotification.js
│ │ │ ├── dismissGroupNotification.js
│ │ │ ├── groupNotification.js
│ │ │ ├── kickoffGroupMemberNotification.js
│ │ │ ├── notificationMessageContent.js
│ │ │ ├── quitGroupNotification.js
│ │ │ └── recallMessageNotification.js
│ │ ├── persistFlag.js
│ │ ├── protomessage.js
│ │ ├── protomessageContent.js
│ │ ├── sendMessage.js
│ │ ├── textMessageContent.js
│ │ ├── unknownMessageContent.js
│ │ ├── unsupportMessageContent.js
│ │ ├── videoMessageContent.js
│ │ └── websocketprotomessage.js
│ ├── model/
│ │ ├── conversation.js
│ │ ├── conversationInfo.js
│ │ ├── conversationType.js
│ │ ├── groupInfo.js
│ │ ├── groupMember.js
│ │ ├── groupMemberType.js
│ │ ├── groupType.js
│ │ ├── protoConversationInfo.js
│ │ ├── stateConversationInfo.js
│ │ ├── stateSelectChatMessage.js
│ │ ├── unReadCount.js
│ │ └── userInfo.js
│ ├── store/
│ │ └── localstore.js
│ ├── utils/
│ │ ├── StringUtil.js
│ │ ├── aes.js
│ │ ├── logger.js
│ │ └── timeUtils.js
│ └── websocketcli.js
└── static/
├── .gitkeep
└── css/
└── reset.css
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"plugins": ["transform-runtime"],
"comments": false,
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": [ "istanbul" ]
}
}
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: .postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
// to edit target browsers: use "browserlist" field in package.json
"autoprefixer": {}
}
}
================================================
FILE: LICENSE
================================================
Creative Commons Attribution-NonCommercial 3.0 Unported
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License.
b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License.
c. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership.
d. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast.
f. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work.
g. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
h. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images.
i. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium.
2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections;
b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved, including but not limited to the rights set forth in Section 4(d).
4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested.
b. You may not exercise any of the rights granted to You in Section 3 above in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation. The exchange of the Work for other copyrighted works by means of digital file-sharing or otherwise shall not be considered to be intended for or directed toward commercial advantage or private monetary compensation, provided there is no payment of any monetary compensation in connection with the exchange of copyrighted works.
c. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and, (iv) consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties.
d. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License;
ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License if Your exercise of such rights is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b) and otherwise waives the right to collect royalties through any statutory or compulsory licensing scheme; and,
iii. Voluntary License Schemes. The Licensor reserves the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License that is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(c).
e. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law.
Creative Commons Notice
Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License.
Creative Commons may be contacted at http://creativecommons.org/.
================================================
FILE: README.md
================================================
[](https://gitee.com/comsince/vue-chat)
[](https://github.com/comsince/vue-chat)
# 飞享

**NOTE:** [飞享]IM系统开始进行商业化探索,欢迎有需要的`个人`,`企业`, `工作室`使用,关于授权合作事项,请咨询QQ `1282212195`
该项目是`飞享`聊天系统客户端源码vue即时通讯web端实现,使用websocket进行消息通讯,支持文本,图片类型发送,支持实时音视频,支持音视频与[android-chat](https://github.com/fsharechat/android-chat)客户端互通
# 项目截图
* 消息提示

* 文字消息

* 图片消息

* 视频消息

# 项目演示
* [项目公测地址](https://chat.comsince.cn)
* 请选择其中任何一个帐号密码进行登录即可
```properties
帐号:13800000000, 13800000001, 13800000002
密码:556677
```
* 暂时停止手机验证码注册登录,后续开通QQ群里面通知
## 版本规划
### V1.0.0
* 登录认证流程
* 实现朋友列表展示,用户信息获取
* 会话信息拉取,会话消息缓存
* 纯文本消息通讯
* 支持图片,视频消息展示
* 群会话功能
### V1.0.1
* 增加全屏幕模式支持,点击用户头像即可切换

### V1.0.2
* 计划增加一对一音视频聊天功能
* 实现与[android](https://github.com/fsharechat/android-chat)客户端音视频互通
### V1.0.3
* 增加好友搜索,好友添加功能,形成功能闭环
### V1.0.4
* 群组用户列表功能
### V1.0.5
* 增加websocket异步回调接口
* 增加创建群组功能
* 退出群聊
* 撤回消息
* 群组踢人与拉人
* 修改群名称

### V1.0.6
* 增加解散群组的功能
* 优化群组退出与解散交互体验
* 对于解散的群组与退出的群组,做删除会话处理
### V1.0.7
* 增加删除消息的功能
* 增加转发消息
### V1.0.8
* 支持缩略图传输,防止android 客户端转发图片报错
### V1.0.9
* 支持缩略图显示
### V1.0.14
* 修复群组管理员撤回其他成员发送消息的问题
### V1.1.0
* 加入群组音视频功能
### V1.1.3
* 增加文件发送功能
* 增加通知短音提示
* 增加音视频通话铃声提示
* 增加截图粘贴发送功能
### V1.1.4
* 限制每条会话的消息条数,发送消息时才会删除过多的消息,接收消息时有可能会删除历史未读消息,所以接收时暂不删除过多的消息
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:9080
npm run dev
# 运行请先检查如下配置:TCP服务配置,HTTPS配置,是否支持WSS,是否支持HTTPS,HTTP监听端口8081,HTTPS监听端口8443
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
## 参考项目
* [Vue-chat](https://github.com/han960619/Vue-chat/)
## 依赖组件
* [常用的 vue 视频插件](https://wangchaoke.cn/?p=372)
* [西瓜播放器](http://h5player.bytedance.com/gettingStarted)
* [图标Icon支持](https://www.iconfont.cn/manage/index?spm=a313x.7781069.1998910419.11&manage_type=myprojects&projectId=1698562)
## 推荐项目
* [vue-wechat](https://github.com/zhaohaodang/vue-WeChat)
* [vue-chat](https://github.com/aermin/vue-chat)
* [QRCodeLogin](https://github.com/HeyJC/QRCodeLogin/blob/master/Web/auth/src/components/Input.vue) 说明二维码和密码登录的切换操作
## 开源协议
本项目使用非商业性署名协议,禁止演绎[Creative Commons Attribution Non Commercial 3.0 Unported](LICENSE)
## 一次性赞助
但是随着项目的增长,也需要相应的资金支持,你可以通过以下方式来赞助此项目
| 支付宝 | 微信|
| :--------: | :--------:|
| | |
## QQ 群交流
| QQ群 |
| :--------: |
| |
## 技术支持
如果公司采用本项目或者需要有商业需求,需要二次开发,提供技术支持,联系QQ:`1282212195`
================================================
FILE: build/build.js
================================================
require('./check-versions')()
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
================================================
FILE: build/check-versions.js
================================================
var chalk = require('chalk')
var semver = require('semver')
var packageConfig = require('../package.json')
var shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
var versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
var warnings = []
for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
================================================
FILE: build/dev-client.js
================================================
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})
================================================
FILE: build/dev-server.js
================================================
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
console.log('> Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
var server = app.listen(port)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}
================================================
FILE: build/utils.js
================================================
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
publicPath: '../../'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
================================================
FILE: build/vue-loader.conf.js
================================================
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap,
extract: isProduction
})
}
================================================
FILE: build/webpack.base.conf.js
================================================
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('/node_modules/webrtc-adapter/src')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
}
================================================
FILE: build/webpack.dev.conf.js
================================================
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
var path = require('path');
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true,
favicon: path.resolve('favicon.ico'),
}),
new FriendlyErrorsPlugin()
]
})
================================================
FILE: build/webpack.prod.conf.js
================================================
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
favicon: path.resolve('favicon.ico'),
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
================================================
FILE: config/dev.env.js
================================================
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
================================================
FILE: config/index.js
================================================
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
port: 9080,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
}
================================================
FILE: config/prod.env.js
================================================
module.exports = {
NODE_ENV: '"production"'
}
================================================
FILE: index.html
================================================
飞享IM
================================================
FILE: index.md
================================================
================================================
FILE: package.json
================================================
{
"name": "wechat",
"version": "1.1.6",
"description": "基于fshare的开源即时通讯web客户端",
"author": "comsince",
"private": true,
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"build": "node build/build.js"
},
"dependencies": {
"@wcjiang/notify": "^2.0.12",
"axios": "^0.19.0",
"crypto-js": "^3.1.9-1",
"element-ui": "^2.13.0",
"kurento-utils": "^6.14.0",
"pinyin": "^2.9.0",
"qiniu-js": "^2.5.5",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"uuid-js": "^0.7.5",
"v-viewer": "^1.5.1",
"vue": "^2.5.2",
"vue-axios": "^2.1.4",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"webrtc-adapter": "^7.5.1",
"xgplayer": "^2.4.7",
"xgplayer-vue": "^1.1.5"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"webpack-bundle-analyzer": "^2.2.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"url-loader": "^0.5.8",
"vue-loader": "^11.3.4",
"vue-style-loader": "^2.0.5",
"vue-template-compiler": "^2.2.6",
"webpack": "^2.3.3",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
================================================
FILE: src/App.vue
================================================
================================================
FILE: src/assets/fonts/iconfont.css
================================================
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1592274269382'); /* IE9 */
src: url('iconfont.eot?t=1592274269382#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAy4AAsAAAAAF1wAAAxrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCGNAqcCJY0ATYCJANgCzIABCAFhG0HgkgbjhMjEaac1Znsrwu4Q2pxlyQy2IObrVRoQS6vgYCib9QxOJH4IiO5qF+ow/eNfD6asz87syHZ3YSNIpaKUxrUQ1PFrWIWapq6UlGHe0epaDhfeu6XbnWqb5qigXvxVnttfwLZhMWW2FkAIGzCGPqKGBjm9r1elL5r9UfHVLmp/Mh2kRVXwMsKwuZGZq3NEiDU5pzCUfHDAgj4/+de7UtZdgxvGFdhembVe0k+PEhPbzk/I0qJX4egajeFx89jXjnD/AGAA9SrmnUzzg8CRpseqIuCj4xV3x0IAAGBCAax9+yXDzUYFCd0GzlsSA3UsUqwkgkE6jJVznUZiAs81NwSrhHAKnf8ydcII2qAA0+h7il7cMZApCi0dTV1/u9EOXWirj8PwPl6AAUQDIABnJy7iQigPgt2hZCnMrQA6AyreJNqhSoqxV8JUtopwUqokqikKg6lUClRBivDlBHKktYuraWtq///v1Zp38KUVQw9dOUTdNalgwtDCm3/hQdoIcMACySYYIYAEWp4gUIDFYzwhAcIdOChBwMHEBXoKgEAaS8bvQCFmiZoAUVlggwoviYYAMXPBAug+JsgAUoQwHQn2wEwI4MBCMhQACIyEYAamYI9vkwFQJEOABpkIRxzkiUAjMjBADyRQwF4IIcBIMgRAHTIJZjAA61dTNADraUmMKB1dQYclPC2r2MD0AtgYQDfDNx1rqDa5SG4BI+BM503teBSvTkQWcdkWTDAn6lWkr8syUxzFuvVVtKw9hLxN8tDzKgykzR+GslDI795b7OfJHkbzWZvKdggyRrewztohu5skz+3/Umk54WnQXRHc3H1uqWWDYTW1Ztr1m8a6wndjHNPAti2pgjd+eZAfufT6Gn8hg0W3bp1JrZ+vdlz48ZSad/RWqvfGalvbOgnPwbFPsRDvyopGdWosbBWSwmqleKswhpUpUCw5vvVmqbMD2pVVQuFNSA2JcaNTY75lOwV/UzZcC8SVDdF+gmw9k2l4yR1AHfjp8PSpmfEodQILz53R2rF2rhNu9PKvdQ60Sq/emdaZe36+C01f1Pj9ZU2DGBOhannK87sci8urTt1PO9lEu5qg6GBDIMsI0SmQUVmud1uo6J75VN6NCY2V0caKBEXcwp729cOq8KuLcKV3/OK+XM2AhF/p24tQgeS7ms/eqX6Am67jM/P2lBtDOMJA93QVVjhdrhFzLYrtpVmwFZvmRMpwhnsk72aSIJQDSF1wxxT0OhOb7S5Ba1ALweRBwT8Dqn8Lvul9xqBN8gUtUFIZfBHZuxM2Q5ckZNrVe6uHZViZEGwPXNyof4Gyc7EZS+fMDqpDxk4JDurzJwhxBxc9SKi6ECLSvpCJIgWyEzewOwortYPzon1dkMr8LJwMOj3+9DAqweg6uJXZxtaweAqzA2qFZHJAzQAa6hvUFVWdnV5Ze0T2ucIhYHhY3DlBbWF8bk0JEoBzNxnPo7Wxr5BtMrdIJ7zarVi/2VEM+1eIDTHpKl2fDnMP1TrAwg/W968IW8TiBoESNvZebCe/xCbZ+/OMffnHiBzBtuCgQjxcdKYbMnnJ3cEvtGF0NU8Sk5updHND/Lk4/8oiVJLcqM8H5ZnZ2X20BZjB1ETDkCMirKstZ3BvW6rw31ZDGbAIYFlhdB8/nJhQEax+0mfwLbYvfJaZFy39hyBqKrmDOzS9/UpzO9/fWUzuF6tCwOiYBdzEYUWKKRxjeGs9zPtdUCn5wotMp7v6exp8Z2IfJpCn7Yv+x0NOO8CR+u8r+6PsNx42pbSV5fIehWjtA1W7Iac14SGenlTpyawOVx5Vj2jUcQuj7Vt8jghd/xC5KPnAXXbw+74Fj9xa3/Qup+44fUz5L0DdLkv5T6eJ15e3ctDXdvFDZENiE7PGRvu8d4jfOx7hrPRjPeaRtd7V6Pm0H7Drve20FCbNSzC53WYfHKzOFncfJLzcm/1U/STzQvd4nf4sFbvq9c661FH/lXFKSkzirdar+VcUWYm3tS28TPo7HH11+N1SatcYC2QW8UwP02IEDbviti1WTjtGG/tFqFxRbg0Ilsk5jkPG6cY6w1TrPXyFFJPCulnB/IUPM+Pjinu634zX7/YIK5LBAtPnM50+nP8P3+9veLQzTZ+1puHDkkGP046dP3wIAPCal/VcmtjF8oLFy1c2mUprNSZ3RDcaDnGFIUd06Aolh3G0LadoD89JXrKzRPdXP3Za6v1NWl2qmCvhdVmr7t9chXt3nZeLd1bQsx49FGU74KRmDbo3/uSSFI8bVwFKSd2HSmOjMf5F9tO3IUawdQk2nRkxCbXJE633byaGyuadHdh/KBxe+I+DCousgNi8yA/mtj54rwgJLniJk0SrlquCs5pDZYGIXZbb89ed7QdlJ9U621vl+1EDjdp+/ZeiU2s9cjNrct5+D3BzX+LR8VGgWreidJ17tebrUxXj8bJJGgyVzN3Xi3RRPbiarjauXNrOQjRODdWlerwvXHAUGqoCOufGXlq1PcPj8yMfybD51y6O3xz/d1OvnccUro2ddjEdClVe/uQz8JR/97crU+IFRMO6trq+Lr6j35XVfYy3rk/bGTzDwU4cdCFDVrDT/dfauNGfGCG1GJRVG62P5/WbBDZP7dbONa1520/P7McQT+n32D//MOs9PUTxZKE9A22N33gSppGV9+ovLyMzBknFZlUEnx+EIk6vaowg6STqjxNKi/94AOJqMvrpgysaHM1rXpcBD2Td12vHPzF14Ls5R7jMpk0bLCSHFwzxN3dLX1XVV22+a6H5uMkS8LontP3s/jv+lsa+C27QGoKD/eP7Tp7ht+MRdK4blVh+RVeVdwrZv2Hv8YuBH6rvZNQwZV6z51YbosLWZR4qU943nZSRvepXjyiktgfle5jR9SfY3enopJ2mQtXFQT27bv6iHXs0fbhfarHFvNl+GLy7YdzWzUjjYOnpzc9vW0LPYr4UUVk4kRSBKG8iINALStsNTyNFBUT0qmSpZ2mFxeTVMLvP6ScpeanVQske8SIbOJym93CqcxMP7FHxqp142JHz5y0yy3kZCxNsU1uezJsQZfY9KguOZWCO9I2lCO7bghiWXi/eSnxBfMj1qTNz+hc6NRk+TSMGia0aDIDHf72YcMyAnoGZIpui1tcQa9e5VZoWhBts6bae5/BYP6V1fpKpnmq+FdhzBocS7cnzJiR7pkXYfe0f6e6z9RqJtL3BSUlQ3AhoMFmt1MHhdv87I124Tvzd4JdO/XjhqbFgY7AxU0NH0/VFog5oWKB0W7Xfi98ZxpSp3o5h5i+N01Zu8bRD1m7dvL8NX/vcptbxL+bjvUtbIYv/nOQykrScyZxUOUgMeRAPRuurMpGX/3Js9ZPuVIwderZzb/jhQt43G/0N/Xxv70pp4ysFf+VIqUfxLXi/yf9T/4vBsIPJw9I3yBuXOHBpS8m9ekUkOc3y2Ptx/CrGsjSAgYW/5JinHfecDhv8f0M/6Zkxl1GOGZt/B7A/8oW6usuOE6baW6bT3bSDlAb6Bzu/3gwJ0GdYsHt6zSp4cPvaFjZhHzuY3Yzj2fIGQzc/eAGvUXtBIcYaCENH++j7ev616i270nLaeCEw5zS8OcRSsSaE1zMX9qhwJpeWqo8E35Vi799T3mry6xwk/OphvoH9moC/F11ah7F7pKqypvd/JumPdoib4yCOuAACLof8Fd1nQEtc3LNtZ0/7q45xBw8EIAp1GhvsPxgzENEBFZBjWQsoBscNxZhxHhQEKYB0BULDphAxgnMQYcbmELGBwbLd2MeFnyLVZAJxQIKiTGmiI4rxMGrAe5AsGjDMKZSGjVu4HX/AuVzbqJJd/4fGItb2XV4lWz+hBJMHxF2qW6ck0waKtjH4d4gz4lVhlKIXZg4V70sFjIvMoypqA3waoA7e3nBRJuSGFMpT+4GqT//Bcrn3JTs97bkHxiLPnP9b8J+axifoqy137modqluOOFJZu5qqGA+hAe5biCmyh8shdiFkhZB9WIhjifrinB8t3hNnAMgQH1E7w0hFVXTDdOyHdfzz+/cvXf/wcNHj3UmJFhhOXeW5WWc+EB6KvNW8WgLaorcJn6L515ZWczWD8SsLVW1/N6Gm/YGC91uNKovIAcHQWTGOXFRWx9YylGMJLdUqkGDiGOvqqhuyF+PN5x0uZdwirJ+W8cfsscxWKIA6kVY2mYgstX/+oGY0BXz2JJ43u1hgI4lbz11nL+nDqvK6rsrKFN3vvJc+IpaDQ==') format('woff2'),
url('iconfont.woff?t=1592274269382') format('woff'),
url('iconfont.ttf?t=1592274269382') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('iconfont.svg?t=1592274269382#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-shipin:before {
content: "\e669";
}
.icon-shanchu-fangkuang:before {
content: "\e791";
}
.icon-zengjiashuzi:before {
content: "\e641";
}
.icon-jianshanchu-xianxingfangkuang:before {
content: "\e75a";
}
.icon-zengjia2:before {
content: "\e689";
}
.icon-yichu:before {
content: "\e656";
}
.icon-zengjia:before {
content: "\e668";
}
.icon-delete-br:before {
content: "\e63d";
}
.icon-loading-solid:before {
content: "\e647";
}
.icon-fasongshibai:before {
content: "\e62c";
}
.icon-pengyou1:before {
content: "\e631";
}
.icon-yaoqinghaoyou:before {
content: "\e61c";
}
.icon-jiahaoyou:before {
content: "\e603";
}
.icon-ai-video:before {
content: "\e66b";
}
.icon-biaoqing:before {
content: "\e666";
}
.icon-dkw_xiaoxi:before {
content: "\e606";
}
.icon-dianhua:before {
content: "\e729";
}
.icon-pengyou:before {
content: "\e61a";
}
.icon-sousuo:before {
content: "\e659";
}
.icon-tuichu:before {
content: "\e61f";
}
.icon-tupian:before {
content: "\e623";
}
.icon-wenjian:before {
content: "\e61b";
}
.icon-guaduan:before {
content: "\e640";
}
================================================
FILE: src/assets/fonts/iconfont.js
================================================
!function(c){var t,i,o,a,l,h,e,s=' ',n=(t=document.getElementsByTagName("script"))[t.length-1].getAttribute("data-injectcss");if(n&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}function d(){h||(h=!0,a())}i=function(){var c,t,i,o,a,l=document.createElement("div");l.innerHTML=s,s=null,(c=l.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",t=c,(i=document.body).firstChild?(o=t,(a=i.firstChild).parentNode.insertBefore(o,a)):i.appendChild(t))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(i,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),i()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(a=i,l=c.document,h=!1,(e=function(){try{l.documentElement.doScroll("left")}catch(c){return void setTimeout(e,50)}d()})(),l.onreadystatechange=function(){"complete"==l.readyState&&(l.onreadystatechange=null,d())})}(window);
================================================
FILE: src/assets/fonts/iconfont.json
================================================
{
"id": "1698562",
"name": "vue-chat",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "5100253",
"name": "视频",
"font_class": "shipin",
"unicode": "e669",
"unicode_decimal": 58985
},
{
"icon_id": "4425816",
"name": "删除-方框",
"font_class": "shanchu-fangkuang",
"unicode": "e791",
"unicode_decimal": 59281
},
{
"icon_id": "4770755",
"name": "增加数字",
"font_class": "zengjiashuzi",
"unicode": "e641",
"unicode_decimal": 58945
},
{
"icon_id": "6129045",
"name": "9减、删除-线性方框",
"font_class": "jianshanchu-xianxingfangkuang",
"unicode": "e75a",
"unicode_decimal": 59226
},
{
"icon_id": "8358855",
"name": "增加4",
"font_class": "zengjia2",
"unicode": "e689",
"unicode_decimal": 59017
},
{
"icon_id": "5253606",
"name": "移除",
"font_class": "yichu",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "1301395",
"name": "增加",
"font_class": "zengjia",
"unicode": "e668",
"unicode_decimal": 58984
},
{
"icon_id": "1272671",
"name": "移除",
"font_class": "delete-br",
"unicode": "e63d",
"unicode_decimal": 58941
},
{
"icon_id": "4581221",
"name": "发送中",
"font_class": "loading-solid",
"unicode": "e647",
"unicode_decimal": 58951
},
{
"icon_id": "3398713",
"name": "发送失败",
"font_class": "fasongshibai",
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "6659620",
"name": "朋友",
"font_class": "pengyou1",
"unicode": "e631",
"unicode_decimal": 58929
},
{
"icon_id": "3315144",
"name": "邀请好友",
"font_class": "yaoqinghaoyou",
"unicode": "e61c",
"unicode_decimal": 58908
},
{
"icon_id": "6683016",
"name": "加好友",
"font_class": "jiahaoyou",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "795513",
"name": "视频",
"font_class": "ai-video",
"unicode": "e66b",
"unicode_decimal": 58987
},
{
"icon_id": "842937",
"name": "表情",
"font_class": "biaoqing",
"unicode": "e666",
"unicode_decimal": 58982
},
{
"icon_id": "2078817",
"name": "dkw_消息 ",
"font_class": "dkw_xiaoxi",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "2967254",
"name": "符号-电话",
"font_class": "dianhua",
"unicode": "e729",
"unicode_decimal": 59177
},
{
"icon_id": "4166140",
"name": "朋友",
"font_class": "pengyou",
"unicode": "e61a",
"unicode_decimal": 58906
},
{
"icon_id": "5582330",
"name": "搜索",
"font_class": "sousuo",
"unicode": "e659",
"unicode_decimal": 58969
},
{
"icon_id": "5893499",
"name": "退出",
"font_class": "tuichu",
"unicode": "e61f",
"unicode_decimal": 58911
},
{
"icon_id": "7588121",
"name": "图片",
"font_class": "tupian",
"unicode": "e623",
"unicode_decimal": 58915
},
{
"icon_id": "10099699",
"name": "文件",
"font_class": "wenjian",
"unicode": "e61b",
"unicode_decimal": 58907
},
{
"icon_id": "10515201",
"name": "挂断",
"font_class": "guaduan",
"unicode": "e640",
"unicode_decimal": 58944
}
]
}
================================================
FILE: src/components/chatlist/chatlist.vue
================================================
{{item.name ? item.name : ""}}
{{item.conversationInfo.timestamp | getTimeStringAutoShort2}}
{{processageGroupMessage(item)}}
{{item.conversationInfo.unreadCount ? item.conversationInfo.unreadCount.unread : 0}}
================================================
FILE: src/components/friendlist/friendlist.vue
================================================
{{item.initial}}
{{newFriendRequestCount}}
================================================
FILE: src/components/info/info.vue
================================================
{{selectedFriend.nickname}}
{{friendName(scope.row.from)}}
{{scope.row.reason}}
{{requestBtnMessage(scope.row)}}
{{selectedFriend.nickname}}
{{selectedFriend.signature}}
地   区 {{selectedFriend.area}}
微信号 {{selectedFriend.wxid}}
发消息
================================================
FILE: src/components/menu/addtip.vue
================================================
================================================
FILE: src/components/menu/groupInfo.vue
================================================
{{item.displayName}}
================================================
FILE: src/components/menu/personalCard.vue
================================================
================================================
FILE: src/components/menu/relayMessage.vue
================================================
================================================
FILE: src/components/menu/rightMenu.vue
================================================
================================================
FILE: src/components/message/message.vue
================================================
{{loadingDes}}
{{item.timestamp | getTimeStringAutoShort2}}
{{notificationContent(item)}}
{{notificationContent(item)}}
{{item.content.content}}
{{showUserName(item.from)}}
================================================
FILE: src/components/mycard/mycard.vue
================================================
================================================
FILE: src/components/search/search.vue
================================================
================================================
FILE: src/components/text/text.vue
================================================
================================================
FILE: src/constant/index.js
================================================
export const WS_PROTOCOL = 'wss';
export const WS_IP = 'backend-websocket.fsharechat.cn/ws';
export const HTTP_IP = 'backend-http.fsharechat.cn';
//websocket端口,请不要更改
export const WS_PORT = 9326;
export const HEART_BEAT_INTERVAL = 25 * 1000;
export const RECONNECT_INTERVAL = 30 * 1000;
export const BINTRAY_TYPE = 'blob';
//signal
export const CONNECT = 'CONNECT';
export const DISCONNECT = 'DISCONNECT';
export const CONNECT_ACK = 'CONNECT_ACK';
export const PUBLISH = 'PUBLISH';
export const PUB_ACK = 'PUB_ACK';
//subsignal
export const FRP = 'FRP';
export const FP = 'FP';
export const FALS = 'FALS';
export const UPUI = 'UPUI';
export const GPGI = 'GPGI';
export const GPGM = 'GPGM';
export const GAM = 'GAM';
export const GC = 'GC';
export const GMI = 'GMI';
export const GKM = 'GKM';
export const GQ = 'GQ';
export const GD = 'GD';
export const MP = 'MP';
export const MS = "MS";
export const MN = "MN";
export const MR = "MR";
export const RMN = "RMN";
export const GQNUT = "GQNUT";
export const GMURL = "GMURL";
export const US = "US";
export const FAR = "FAR";
export const FRN = "FRN";
export const FHR = "FHR";
export const FN = "FN";
export const MMI = "MMI";
export const LRM = "LRM";
export const HTTP_HOST = "https://"+HTTP_IP + "/"
export const LOGIN_API = HTTP_HOST + "login";
export const SNED_VERIFY_CODE_API = HTTP_HOST + "send_code";;
export const KEY_VUE_DEVICE_ID = 'vue-device-id';
export const KEY_VUE_USER_ID = 'vue-user-id';
export const KEY_VUE_TOKEN = 'vue-token';
//userId 这里为了演示静态登录,由于还没有登录界面所以暂时使用静态userid
export const USER_ID = 'TYTzTz33';
export const CLINET_ID = 'bccdb58cfdb34d861576810441000';
//token
export const TOKEN = '6Yz2rQDrtRPRc3j9PesLy0De17uX2RlVcvkxU/UmGEaMamd/kaagwWNThIWSGMd6SPVHxLeynho03sJWdbm7wFMRO8VTKf5Wogv7l7gKLsq81mswRha3j6FMdDVHVJ+MLJrVjrThkqXrK1rHwsZvGxpqSGcekHIggI1UEEJSXyQ=';
//是否使用七牛上传文件
export const UPLOAD_BY_QINIU = false;
export const ERROR_CODE = 400;
export const SUCCESS_CODE = 200;
//conversation
export const CONVERSATION_MAX_MESSAGE_SIZE = 50;
================================================
FILE: src/main.js
================================================
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import './permission' // permission control
Vue.config.productionTip = false
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
/* eslint-disable no-new */
const vm = new Vue({
el: '#app',
router,
store,
template: ' ',
components: { App }
})
================================================
FILE: src/page/chat/chat.vue
================================================
================================================
FILE: src/page/friend/friend.vue
================================================
================================================
FILE: src/page/friend/searchfriend.vue
================================================
{{scope.row.displayName}}
{{scope.row.mobile}}
================================================
FILE: src/page/group/creategroup.vue
================================================
{{groupDialogTitle}}
{{checkFriendTips}}
================================================
FILE: src/page/group/groupVideoCall.vue
================================================
接通中...
取消
拒绝
接听
00:00
挂断
================================================
FILE: src/page/login/login.vue
================================================
================================================
FILE: src/page/main.vue
================================================
================================================
FILE: src/permission.js
================================================
import router from './router'
import store from './store'
const whiteList = ['/login'] // 不重定向白名单
router.beforeEach((to, from, next) => {
var token = localStorage.getItem('vue-token');
console.log('match route token '+token+" to "+to.path +" from "+from.path +" next "+next.path);
if (token) {
if (to.path === '/login') {
next({ path: '/conversation' })
} else {
next();
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next('/login')
}
}
})
================================================
FILE: src/router/index.js
================================================
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
mode: 'history',
// 共三个页面: 聊天页面,好友页面,个人简历分别对应一下路由
routes: [
{
path: '/login',
component: require('@/page/login/login.vue')
},
{
path: '/',
component: require('@/page/main.vue'),
redirect: 'conversation',
children: [{
path: 'conversation',
name: 'conversation',
component: require('@/page/chat/chat.vue'),
hidden:true,
},
{
path: 'friend',
name: 'friend',
component: require('@/page/friend/friend.vue'),
hidden:true,
}],
},
],
linkActiveClass: 'active'
})
// router.push({ path: '/chat' });
export default router
================================================
FILE: src/store.js
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import router from './router'
import VueWebSocket from './websocket';
import VoipClient from './webrtc/voipclient'
import GroupCallClient from './webrtc/groupCallClient'
import {WS_PROTOCOL,WS_IP,WS_PORT,HEART_BEAT_INTERVAL,RECONNECT_INTERVAL,BINTRAY_TYPE, KEY_VUE_USER_ID, KEY_VUE_TOKEN, CONVERSATION_MAX_MESSAGE_SIZE} from './constant/index'
import StateConversationInfo from './websocket/model/stateConversationInfo';
import StateChatMessage from './websocket/model/stateSelectChatMessage'
import Message from './websocket/message/message';
import ProtoMessage from './websocket/message/protomessage';
import ConversationType from './websocket/model/conversationType';
import LocalStore from './websocket/store/localstore';
import ProtoConversationInfo from './websocket/model/protoConversationInfo';
import UnreadCount from './websocket/model/unReadCount';
import StateSelectChateMessage from './websocket/model/stateSelectChatMessage';
import Notify from '@wcjiang/notify';
import MessageConfig from './websocket/message/messageConfig';
import ChatManager from './websocket/chatManager';
import ProtoMessageContent from './websocket/message/protomessageContent';
import Logger from './websocket/utils/logger';
import RecallMessageNotification from './websocket/message/notification/recallMessageNotification';
import MessageStatus from './websocket/message/messageStatus';
Vue.use(Vuex)
//获取当前时间
const now = new Date();
const state = {
// 输入的搜索值
searchText: '',
// 当前登录用户
user: {
name: 'ratel',
img: 'static/images/vue.jpg'
},
// 好友列表
friendlist: [
{
id: 0,
wxid: "new", //微信号
initial: '新的朋友', //姓名首字母
img: 'static/images/newfriend.jpg', //头像
signature: "", //个性签名
nickname: "新的朋友", //昵称
sex: 0, //性别 1为男,0为女
remark: "新的朋友", //备注
area: "", //地区
}
],
friendIds: [],
friendDatas: [],
//emoji表情
emojis: [
{ file: '100.gif', code: '/::)', title: '微笑',reg:/\/::\)/g },
{ file: '101.gif', code: '/::~', title: '伤心',reg:/\/::~/g },
{ file: '102.gif', code: '/::B', title: '美女',reg:/\/::B/g },
{ file: '103.gif', code: '/::|', title: '发呆',reg:/\/::\|/g },
{ file: '104.gif', code: '/:8-)', title: '墨镜',reg:/\/:8-\)/g },
{ file: '105.gif', code: '/::<', title: '哭',reg:/\/::', title: '高兴',reg:/\/::>/g },
{ file: '129.gif', code: '/::,@', title: '闲',reg:/\/::,@/g },
{ file: '130.gif', code: '/:,@f', title: '努力',reg:/\/:,@f/g },
{ file: '131.gif', code: '/::-S', title: '骂',reg:/\/::-S/g },
{ file: '133.gif', code: '/:,@x', title: '秘密',reg:/\/:,@x/g },
{ file: '134.gif', code: '/:,@@', title: '乱',reg:/\/:,@@/g },
{ file: '135.gif', code: '/::8', title: '疯',reg:/\/::8/g },
{ file: '136.gif', code: '/:,@!', title: '哀',reg:/\/:,@!/g },
{ file: '137.gif', code: '/:!!!', title: '鬼',reg:/\/:!!!/g },
{ file: '138.gif', code: '/:xx', title: '打击',reg:/\/:xx/g },
{ file: '139.gif', code: '/:bye', title: 'bye',reg:/\/:bye/g },
{ file: '142.gif', code: '/:handclap', title: '鼓掌',reg:/\/:handclap/g },
{ file: '145.gif', code: '/:<@', title: '什么',reg:/\/:<@/g },
{ file: '147.gif', code: '/::-O', title: '累',reg:/\/::-O/g },
{ file: '153.gif', code: '/:@x', title: '吓',reg:/\/:@x/g },
{ file: '155.gif', code: '/:pd', title: '刀',reg:/\/:pd/g },
{ file: '156.gif', code: '/:', title: '水果',reg:/\/:/g },
{ file: '157.gif', code: '/:beer', title: '酒',reg:/\/:beer/g },
{ file: '158.gif', code: '/:basketb', title: '篮球',reg:/\/:basketb/g },
{ file: '159.gif', code: '/:oo', title: '乒乓',reg:/\/:oo/g },
{ file: '195.gif', code: '/:circle', title: '跳舞',reg:/\/:circle/g },
{ file: '160.gif', code: '/:coffee', title: '咖啡',reg:/\/:coffee/g }
],
// 得知当前选择的是哪个对话
selectId: 1,
//选择的会话target
selectTarget: '',
// 得知当前选择的是哪个好友
selectFriendId: 0,
vueSocket: null,
voipClient: null,
groupCallClient: null,
//会话列表
conversations: [],
//消息列表
messages: [],
//搜索用户列表
searchUsers: [],
friendRequests: [],
newFriendRequestCount: 0,
deviceId: '',
userId: '',
token: '',
userInfoList: [],
groupInfoList: [],
tempGroupMembers: [],
notify:'',
inCommingNotify:'',
outGoingNotify:'',
firstLogin: false,
emptyMessage: false,
//修改全屏模式
changeFullScreenMode: false,
appHeight: 638,
visibilityState: 'hidden',
//是否限制音视频对话框
showChatBox: false,
showAudioBox: false,
showSearchFriendDialog: false,
showCreateGroupDialog: false,
showRelayMessageDialog: false,
showGroupCallVideoDialog: false,
groupCallMembers: [],
showGroupInfo: false,
showMessageRightMenu: [],
currentRightMenuMessage: null,
//待请求用户id信息列表
waitUserIds: [],
//0创建群组,1,添加群组人员,2,移除群组人员 3 单聊用户创建群组 4 创建群组音视频聊天
groupOperateState: 0,
groupMemberMap: new Map(),
groupMemberTracker: 0,
isLoadRemoteMessage: false
}
const mutations = {
// 从localStorage 中获取数据
initData (state) {
state.userId = localStorage.getItem('vue-user-id');
state.token = localStorage.getItem('vue-token');
const vueSocket = new VueWebSocket();
state.vueSocket = vueSocket;
//voip client
state.voipClient = new VoipClient(store);
state.groupCallClient = new GroupCallClient(store)
let conversations = LocalStore.getConversations();
if(conversations){
state.conversations = conversations;
}
let messages = LocalStore.getMessages();
if(messages){
state.messages = messages;
}
state.selectTarget = LocalStore.getSelectTarget();
let userInfoList = LocalStore.getUserInfoList();
if(userInfoList){
state.userInfoList = userInfoList;
}
state.notify = new Notify({
effect: 'flash',
interval: 500,
onclick: () => {
console.log('on click');
state.notify.close();
},
audio:{
file: ['/static/audio/notify.mp3']
}
});
state.inCommingNotify = new Notify({
audio:{
file: ['/static/audio/incoming_call_ring.mp3']
}
});
state.outGoingNotify = new Notify({
audio:{
file: ['/static/audio/outgoing_call_ring.mp3']
}
});
},
// 获取搜索值
search (state, value) {
state.searchText = value
},
// 得知用户当前选择的是哪个对话。便于匹配对应的对话框
selectSession (state, value) {
state.selectId = value
},
selectConversation(state,value){
state.selectTarget = value;
//清除未读数
var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === value);
if(stateConversationInfo && stateConversationInfo.conversationInfo.unreadCount){
stateConversationInfo.conversationInfo.unreadCount.unread = 0;
}
},
clearUnreadStatus(state){
var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === state.selectTarget);
if(stateConversationInfo && stateConversationInfo.conversationInfo.unreadCount){
stateConversationInfo.conversationInfo.unreadCount.unread = 0;
}
},
// 得知用户当前选择的是哪个好友。
selectFriend (state, value) {
state.selectFriendId = value
console.log("select friend id "+value);
if(value === 0){
if(state.friendRequests.length == 0){
state.vueSocket.getFriendRequest(LocalStore.getFriendRequestVersion());
} else {
state.newFriendRequestCount = 0;
}
}
},
//更新朋友列表
updateFriendList(state,value){
if(state.friendlist.length === 0){
state.friendlist.push({
id: 0,
wxid: "new", //微信号
initial: '新的朋友', //姓名首字母
img: 'static/images/newfriend.jpg', //头像
signature: "", //个性签名
nickname: "新的朋友", //昵称
sex: 0, //性别 1为男,0为女
remark: "新的朋友", //备注
area: "", //地区
});
}
for(var i in value){
var currentUser = value[i];
if(currentUser.wxid != state.userId){
var friendUid = state.friendIds.find(friendUid => friendUid === currentUser.wxid);
if(friendUid){
var friendData = state.friendDatas.find(friend => friend.friendUid == friendUid)
if(friendData && friendData.alias && friendData.alias != ""){
currentUser.remark = friendData.alias
}
var isExist = false;
for(var friend of state.friendlist){
if(friend.wxid === currentUser.wxid){
isExist = true;
friend.nickname = currentUser.nickname;
friend.img = currentUser.img;
friend.remark = currentUser.remark;
}
}
if(!isExist){
currentUser.id = state.friendlist.length
state.friendlist.push(currentUser);
}
}
}
}
//更新会话信息
for(var stateConversationInfo of state.conversations){
var friend = state.friendlist.find(friend => friend.wxid === stateConversationInfo.conversationInfo.target);
if(friend){
stateConversationInfo.name = friend.remark ? friend.remark: friend.nickname;
stateConversationInfo.img = friend.img;
}
}
//更新消息列表信息
for(var stateChatMessage of state.messages){
var friend = state.friendlist.find(friend => friend.wxid === stateChatMessage.target);
if(friend){
stateChatMessage.name = friend.remark ? friend.remark: friend.nickname;
}
}
},
updateConversationBrief(state){
//更新会话信息
for(var stateConversationInfo of state.conversations){
var friend = state.friendlist.find(friend => friend.wxid === stateConversationInfo.conversationInfo.target);
if(friend){
stateConversationInfo.name = friend.remark ? friend.remark: friend.nickname;
stateConversationInfo.img = friend.img;
} else {
var user = state.userInfoList.find(user => user.uid == stateConversationInfo.conversationInfo.target)
if(user){
stateConversationInfo.name = user.displayName;
stateConversationInfo.img = user.portrait;
}
}
}
},
updateMessageBrief(state){
//更新消息列表信息
for(var stateChatMessage of state.messages){
var friend = state.friendlist.find(friend => friend.wxid === stateChatMessage.target);
if(friend){
stateChatMessage.name = friend.remark ? friend.remark: friend.nickname;
}else {
var user = state.userInfoList.find(user => user.uid == stateChatMessage.target)
if(user){
stateChatMessage.name = user.displayName;
}
}
}
},
updateUserInfos(state,userInfos){
for(let currentUserInfo of userInfos){
if(currentUserInfo.uid === state.userId){
state.user.img = currentUserInfo.portrait;
state.user.name = currentUserInfo.displayName;
}
var isExist = false;
var deleteIndex = 0;
for(var index in state.userInfoList){
var userInfo = state.userInfoList[index];
if(userInfo.uid == currentUserInfo.uid){
isExist = true;
deleteIndex = index;
break;
}
}
if(isExist){
state.userInfoList.splice(deleteIndex,1,currentUserInfo)
} else {
state.userInfoList.push(currentUserInfo);
}
}
this.commit("updateConversationBrief")
this.commit("updateMessageBrief")
},
updateGroupInfos(state,groupInfos){
for(let currentGroupInfo of groupInfos){
var isExist = false;
var deleteIndex = 0;
for(var index in state.groupInfoList){
var groupInfo = state.groupInfoList[index];
if(groupInfo.target == currentGroupInfo.target){
isExist = true;
deleteIndex = index;
break;
}
}
if(isExist){
state.groupInfoList.splice(deleteIndex,1,currentGroupInfo);
} else {
state.groupInfoList.push(currentGroupInfo);
}
}
console.log("group size "+state.groupInfoList.length);
this.commit("updateConversationIntro",groupInfos);
},
getGroupInfo(state,target){
state.vueSocket.getGroupInfo(target,false);
},
getGroupMember(state,groupId){
state.vueSocket.getGroupMember(groupId,true);
},
quitGroup(state,groupId){
state.vueSocket.quitGroup(groupId);
},
deleteConversation(state,groupId){
//为防止再次收到消息,退出群组的发起人不应该在接收任何退出群组消息,防止再次产生会话
var index = -1
for(var i = 0; i 0){
state.selectTarget = state.conversations[0].conversationInfo.target
}
},
updateTempGroupMember(state,groupMembers){
state.tempGroupMembers = groupMembers;
},
// 发送信息
sendMessage (state, sendMessage){
state.isLoadRemoteMessage = false;
var message = Message.toMessage(state,sendMessage);
var protoMessage = ProtoMessage.convertToProtoMessage(message);
console.log("send protomessage "+JSON.stringify(protoMessage));
// if(MessageConfig.isDisplayableMessage(protoMessage)){
// var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === protoMessage.target);
// stateConversationInfo.conversationInfo.lastMessage = protoMessage;
// stateConversationInfo.conversationInfo.timestamp = protoMessage.timestamp;
// var stateChatMessage = state.messages.find(chatmessage => chatmessage.target === protoMessage.target);
// if(!stateChatMessage){
// stateChatMessage = new StateSelectChateMessage();
// stateChatMessage.target = protoMessage.target;
// var friend = state.friendlist.find(friend => friend.wxid === protoMessage.target);
// if(friend != null){
// stateChatMessage.name = friend.nickname;
// }
// stateChatMessage.protoMessages.push(protoMessage);
// state.messages.push(stateChatMessage);
// } else {
// stateChatMessage.protoMessages.push(protoMessage);
// }
// }
this.commit("preAddProtoMessage",protoMessage)
//发送消息到对端
state.vueSocket.sendMessage(protoMessage);
},
//图片,视频类消息,需要先加入消息,然后上传成功后在更新message content
preAddProtoMessage(state,protoMessage){
if(MessageConfig.isDisplayableMessage(protoMessage)){
var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === protoMessage.target);
stateConversationInfo.conversationInfo.lastMessage = protoMessage;
stateConversationInfo.conversationInfo.timestamp = protoMessage.timestamp;
var stateChatMessage = state.messages.find(chatmessage => chatmessage.target === protoMessage.target);
if(!stateChatMessage){
stateChatMessage = new StateSelectChateMessage();
stateChatMessage.target = protoMessage.target;
var friend = state.friendlist.find(friend => friend.wxid === protoMessage.target);
if(friend != null){
stateChatMessage.name = friend.nickname;
}
stateChatMessage.protoMessages.push(protoMessage);
state.messages.push(stateChatMessage);
} else {
//限制单个会话最大消息存储总数
if(stateChatMessage.protoMessages.length > CONVERSATION_MAX_MESSAGE_SIZE){
stateChatMessage.protoMessages.splice(0, stateChatMessage.protoMessages.length - CONVERSATION_MAX_MESSAGE_SIZE );
}
stateChatMessage.protoMessages.push(protoMessage);
}
}
},
updateSendMessage(state,updateMessage){
var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);
if(stateChatMessage){
var protoMessage = stateChatMessage.protoMessages.find(message => message.messageId == updateMessage.messageId);
if(protoMessage){
protoMessage.status = MessageStatus.Sending;
var messagePayload = updateMessage.messageContent.encode();
protoMessage.content = ProtoMessageContent.toProtoMessageContent(messagePayload);
}
}
state.vueSocket.sendMessage(protoMessage);
},
// 选择好友后,点击发送信息。判断在聊天列表中是否有该好友,有的话跳到该好友对话。没有的话
// 添加该好友的对话 并置顶
send (state) {
let result = state.friendlist.find(friend => friend.id === state.selectFriendId)
let stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === result.wxid)
if( !stateConversationInfo ){
state.selectTarget = result.wxid;
var protoConversationInfo = new ProtoConversationInfo();
protoConversationInfo.conversationType = ConversationType.Single;
protoConversationInfo.target = result.wxid;
protoConversationInfo.line = 0;
protoConversationInfo.top = false;
protoConversationInfo.slient = false;
protoConversationInfo.timestamp = new Date().getTime();
protoConversationInfo.unreadCount = new UnreadCount();
protoConversationInfo.lastMessage = null;
var newStateConversationInfo = new StateConversationInfo();
newStateConversationInfo.name = result.remark;
newStateConversationInfo.img = result.img;
newStateConversationInfo.conversationInfo = protoConversationInfo;
state.conversations.unshift(newStateConversationInfo);
} else {
state.selectTarget = stateConversationInfo.conversationInfo.target
}
router.push({ path: '/conversation'})
},
//更新会话列表
updateConversationInfo(state,protoConversationInfo){
var update = false;
var updateStateConverstaionInfo;
var currentConversationInfoIndex;
for(var index in state.conversations){
var stateConverstaionInfo = state.conversations[index];
if(stateConverstaionInfo.conversationInfo.conversationType == protoConversationInfo.conversationType
&& stateConverstaionInfo.conversationInfo.target == protoConversationInfo.target){
update = true;
currentConversationInfoIndex = index;
stateConverstaionInfo.conversationInfo.lastMessage = protoConversationInfo.lastMessage;
stateConverstaionInfo.conversationInfo.timestamp = protoConversationInfo.lastMessage.timestamp;
updateStateConverstaionInfo = stateConverstaionInfo;
break;
}
}
//新消息会话置顶
if(update){
state.conversations.splice(currentConversationInfoIndex,1);
state.conversations.unshift(updateStateConverstaionInfo);
}
if(!update){
updateStateConverstaionInfo = new StateConversationInfo();
updateStateConverstaionInfo.conversationInfo = protoConversationInfo;
//单聊会话
if(protoConversationInfo.conversationType == ConversationType.Single){
var friend = state.friendlist.find(friend => friend.wxid === protoConversationInfo.target);
if(friend != null){
var name = friend.nickname;
var img = friend.img == null ? 'static/images/vue.jpg': friend.img;
updateStateConverstaionInfo.name = name;
updateStateConverstaionInfo.img = img;
} else {
updateStateConverstaionInfo.name = protoConversationInfo.target;
updateStateConverstaionInfo.img = 'static/images/vue.jpg';
}
} else {
//群聊会话
updateStateConverstaionInfo.name = protoConversationInfo.target;
if(!updateStateConverstaionInfo.img){
state.vueSocket.getGroupInfo(protoConversationInfo.target,false);
}
updateStateConverstaionInfo.img = 'static/images/vue.jpg';
}
state.conversations.push(updateStateConverstaionInfo);
}
// 消息是否属于当前会话
var isCurrentConversationMessage = (state.selectTarget === protoConversationInfo.target);
var visibilityStateVisible = (state.visibilityState === 'visible');
console.log("current message "+isCurrentConversationMessage +" visible "+visibilityStateVisible+" first login "+state.firstLogin);
//只显示接收消息,同一用户不同session,不再通知
var isShowSendingMessage = protoConversationInfo.lastMessage.direction === 1;
//更新会话消息未读数
if(!state.firstLogin && (!isCurrentConversationMessage || (isCurrentConversationMessage && !visibilityStateVisible)) && isShowSendingMessage){
//统计消息未读数,注意服务端暂时还没有将透传消息发送过来,原则上这里过来的消息都不是透传消息
var num = updateStateConverstaionInfo.conversationInfo.unreadCount.unread += 1;
var notifyBody = protoConversationInfo.lastMessage.content.searchableContent;
console.log("target "+protoConversationInfo.target+" unread count "+num+ " notify body "+notifyBody);
if(!notifyBody){
notifyBody = ProtoMessageContent.typeToContent(protoConversationInfo.lastMessage.content);
}
//notify 弹框
if(!state.firstLogin){
state.notify.player();
state.notify.notify({
title: updateStateConverstaionInfo.name, // Set notification title
body: notifyBody, // Set message content
icon: updateStateConverstaionInfo.img
});
}
}
},
/**
* 更新会话简介,主要更新会话的名称与图像
*/
updateConversationIntro(state,groupInfos){
for(var groupInfo of groupInfos){
var stateConverstaionInfo = state.conversations.find(stateConverstaionInfo => stateConverstaionInfo.conversationInfo.target === groupInfo.target);
if(stateConverstaionInfo){
console.log("update conversation name "+stateConverstaionInfo.name);
stateConverstaionInfo.name = groupInfo.name;
if(groupInfo.portrait){
stateConverstaionInfo.img = groupInfo.portrait;
}
}
//更新会话标题
var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === groupInfo.target);
if(stateChatMessage){
stateChatMessage.name = groupInfo.name+"("+groupInfo.memberCount+")";
}
}
},
//获取用户当前会话的历史消息
addOldMessage(state,protoMessage){
state.isLoadRemoteMessage = true
console.log("add old message "+protoMessage)
for(var stateChatMessage of state.messages){
if(protoMessage.target == stateChatMessage.target){
var isSameProtoMessage = stateChatMessage.protoMessages.find(message => message.messageId === protoMessage.messageId);
if(!isSameProtoMessage){
stateChatMessage.protoMessages.unshift(protoMessage);
}
}
}
},
addProtoMessage(state,protoMessage){
state.isLoadRemoteMessage = false
//更新用户信息
if(state.waitUserIds.indexOf(protoMessage.from) == -1){
console.log("waiting for get userId "+protoMessage.from);
state.waitUserIds.push(protoMessage.from);
state.vueSocket.getUserInfos([protoMessage.from]);
}
var added = false;
var isExistMessage = false;
for(var stateChatMessage of state.messages){
if(protoMessage.target == stateChatMessage.target){
added = true;
var isSameProtoMessage = stateChatMessage.protoMessages.find(message => message.messageId === protoMessage.messageId);
if(!isSameProtoMessage){
stateChatMessage.protoMessages.push(protoMessage);
} else {
isExistMessage = true;
}
}
}
if(!added){
var stateChatMessage = new StateChatMessage();
var friend = state.friendlist.find(friend => friend.wxid === protoMessage.target);
if(friend != null){
stateChatMessage.name = friend.nickname;
}
stateChatMessage.target = protoMessage.target;
stateChatMessage.protoMessages.push(protoMessage);
state.messages.push(stateChatMessage);
}
//console.log("current message "+protoMessage.messageId +" isExist "+isExistMessage);
if(!isExistMessage){
var protoConversationInfo = new ProtoConversationInfo();
protoConversationInfo.conversationType = protoMessage.conversationType;
protoConversationInfo.target = protoMessage.target;
protoConversationInfo.line = 0;
protoConversationInfo.top = false;
protoConversationInfo.slient = false;
protoConversationInfo.timestamp = protoMessage.timestamp;
protoConversationInfo.lastMessage = protoMessage;
protoConversationInfo.unreadCount = new UnreadCount();
this.commit('updateConversationInfo',protoConversationInfo);
}
},
updateProtoMessageUid(state,updateMessage){
var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);
if(stateChatMessage){
var protoMessage = stateChatMessage.protoMessages.find(message => message.messageId == updateMessage.messageId);
if(protoMessage){
protoMessage.messageUid = updateMessage.messageUid;
return
}
}
//如果切换聊天,需要全局遍历,暂定
for(var stateChatMessage of state.messages){
for(var protoMessage of stateChatMessage.protoMessages){
if(protoMessage.messageId == updateMessage.messageId){
protoMessage.messageUid = updateMessage.messageUid;
}
}
}
},
updateMessageStatus(state,updateMessageStatus){
var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);
if(stateChatMessage){
var protoMessage = stateChatMessage.protoMessages.find(message => message.messageId == updateMessageStatus.messageId);
if(protoMessage){
protoMessage.status = updateMessageStatus.status;
return
}
}
//如果切换聊天,需要全局遍历,暂定。转发消息可能出现这个问题
for(var stateChatMessage of state.messages){
for(var protoMessage of stateChatMessage.protoMessages){
if(protoMessage.messageId == updateMessageStatus.messageId){
protoMessage.status = updateMessageStatus.status;
}
}
}
},
deleteMessage(state,messageId){
var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);
if(stateChatMessage){
var index = -1;
for(var i = 0; i < stateChatMessage.protoMessages.length; i++){
var protoMessage = stateChatMessage.protoMessages[i]
if(protoMessage.messageId == messageId){
index = i;
}
}
console.log("delete index "+index+" messageId "+messageId)
if(index != -1){
stateChatMessage.protoMessages.splice(index,1)
}
}
},
updateMessageContent(state,notifyMessage){
var found = false;
for(var stateMessages of state.messages){
for(var protoMessage of stateMessages.protoMessages){
if(protoMessage.messageUid == notifyMessage.messageUid){
var recallMessageContent = new RecallMessageNotification(notifyMessage.fromUser,notifyMessage.messageUid);
protoMessage.content = recallMessageContent.encode();
found = true
break;
}
}
if(found){
break;
}
}
},
loginOut(state,message){
state.userId = '';
state.token = '';
localStorage.setItem(KEY_VUE_USER_ID,'');
localStorage.setItem(KEY_VUE_TOKEN,'');
state.selectTarget = '',
state.vueSocket.sendDisConnectMessage();
state.vueSocket = null;
state.voipClient = null;
state.conversations = [];
state.messages = [];
state.friendlist = [];
state.friendIds = [];
state.friendDatas = [];
state.selectFriendId = 0;
state.friendRequests = [];
state.waitUserIds = [];
state.userInfoList = [];
state.newFriendRequestCount = 0;
state.groupMemberTracker = 0;
state.showMessageRightMenu = [];
state.emptyMessage = false;
LocalStore.clearLocalStore();
ChatManager.removeOnReceiveMessageListener();
//发送断开消息,清除session,防止同一个设备切换登录导致的验证失败
router.push({path: '/login'})
},
changetFirstLogin(state,value){
console.log("first login "+value);
state.firstLogin = value;
},
getUploadToken(state,value){
state.vueSocket.getUploadToken(value);
},
visibilityChange(state,value){
state.visibilityState = value;
},
searchUser(state,value){
state.vueSocket.searchUser(value);
},
updateSearchUser(state,value){
state.searchUsers = [];
for(var searchUser of value){
var friend = state.friendlist.find(friend => friend.wxid === searchUser.uid);
if(!friend && searchUser.uid !== state.userId){
state.searchUsers.push(searchUser);
}
}
},
sendFriendAddRequest(state,value){
state.vueSocket.sendFriendAddRequest(value);
},
updateFriendRequest(state,value){
for(var newFriendRequst of value){
if(newFriendRequst.status == 0 && new Date().getTime() - newFriendRequst.timestamp > 7* 24 * 60 * 60 * 1000){
console.log("friend request over time")
continue
}
var friendRequest = state.friendRequests.find(friendRequest => friendRequest.from === newFriendRequst.from);
if(friendRequest){
friendRequest.status = newFriendRequst.status;
} else {
if(newFriendRequst.status == 0){
state.newFriendRequestCount += 1;
}
state.friendRequests.push(newFriendRequst);
}
}
},
handleFriendRequest(state,value){
var friendRequest = state.friendRequests.find(friendRequest => friendRequest.from === value.targetUid);
friendRequest.status = 1;
state.vueSocket.handleFriendRequest(value);
},
updateFriendIds(state,friendList){
if(friendList){
state.friendDatas = friendList;
var userIds = [];
for(var i in friendList){
userIds[i] = friendList[i].friendUid;
}
state.friendIds = userIds;
}
},
modifyMyInfo(state,value){
state.vueSocket.modifyMyInfo(value);
},
getUserInfos(state,value){
state.vueSocket.getUserInfos(value);
},
changeEmptyMessageState(state,value){
state.emptyMessage = value;
}
}
const getters = {
currentGroupMembers(){
if(state.groupMemberMap.has(state.selectTarget)){
return state.groupMemberMap.get(state.selectTarget);
} else {
return []
}
},
//筛选会话列表
searchedConversationList(){
return state.conversations.filter(conversationInfo => conversationInfo.name ? conversationInfo.name.includes(state.searchText): false);
},
//当前会话是否为单聊会话
isSingleConversation(){
let stateConversation = state.conversations.find(stateConversation => stateConversation.conversationInfo.target === state.selectTarget);
if(!stateConversation){
return false;
}
return stateConversation.conversationInfo.conversationType === ConversationType.Single;
},
// 筛选出含有搜索值的好友列表
searchedFriendlist () {
//需要根据用户昵称拼音首字母进行分类
var friendMap = new Map();
var noInitFriendList = [];
var allFriendList = [];
for(var friendOrigin of state.friendlist){
var friend = {
id: friendOrigin.id,
wxid: friendOrigin.wxid, //微信号
initial: friendOrigin.initial, //姓名首字母
img: friendOrigin.img, //头像
signature: friendOrigin.signature, //个性签名
nickname: friendOrigin.nickname, //昵称
sex: friendOrigin.sex, //性别 1为男,0为女
remark: friendOrigin.remark, //备注
area: friendOrigin.area, //地区
};
if(friend.id == 0){
continue;
}
if(friend.initial){
var initalFriendList = friendMap.get(friend.initial);
if(initalFriendList){
friend.initial = "";
initalFriendList.push(friend);
} else {
initalFriendList = [];
initalFriendList.push(friend);
friendMap.set(friend.initial,initalFriendList);
}
} else {
noInitFriendList.push(friend);
}
}
if(state.friendlist.length > 0){
allFriendList.push(state.friendlist[0]);
}
if(noInitFriendList.length > 0){
for(var friend of noInitFriendList){
allFriendList.push(friend);
}
}
for(var [key,friendList] of friendMap){
for(var friend of friendList){
allFriendList.push(friend);
}
}
let friends = allFriendList.filter(friend => friend.remark.includes(state.searchText));
return friends;
},
onlyFriendlist(){
let friends = state.friendlist.slice(1,state.friendlist.length);
var listunCheckedFriends = [];
for(var friend of friends){
console.log("friend only initial "+friend.initial);
listunCheckedFriends.push({
id: friend.id,
wxid: friend.wxid,
remark: friend.remark,
img: friend.img,
initial: friend.initial,
checked: false
});
}
return listunCheckedFriends;
},
// 通过当前选择是哪个对话匹配相应的对话
selectedChat (state) {
let chatMessage = state.messages.find(chatMessage => chatMessage.target === state.selectTarget);
console.log("select target "+state.selectTarget)
if(chatMessage == null){
var conversationName = "";
var conversationTarget = '';
if(state.friendlist){
var friend = state.friendlist.find(friend => friend.wxid == state.selectTarget)
if(friend){
conversationName = friend.nickname;
conversationTarget = friend.wxid;
}
}
chatMessage = {
name: conversationName,
target: conversationTarget,
protoMessages: []
}
}
console.log("selectedChat "+chatMessage.name+" target "+chatMessage.target);
return chatMessage
},
// 通过当前选择是哪个好友匹配相应的好友
selectedFriend (state) {
let friend = state.friendlist.find(friend => friend.id === state.selectFriendId);
return friend
},
messages (state) {
let chatMessage = state.messages.find(chatMessage => chatMessage.target === state.selectTarget);
if(chatMessage == null){
return [];
}
return chatMessage.protoMessages;
},
unreadTotalCount(state){
var total = 0;
if(state.conversations){
for(var stateConversationInfo of state.conversations){
if(stateConversationInfo.conversationInfo.unreadCount){
total += stateConversationInfo.conversationInfo.unreadCount.unread;
}
}
}
if(total === 0){
state.notify.faviconClear();
state.notify.setTitle();
state.notify.close();
} else {
state.notify.setFavicon(total)
state.notify.setTitle('你有新的消息未读');
}
return total;
},
}
const actions = {
search: ({ commit }, value) => {
setTimeout(() => {
commit('search', value)
}, 100)
},
selectSession: ({ commit }, value) => commit('selectSession', value),
selectConversation: ({ commit }, value) => commit('selectConversation', value),
clearUnreadStatus: ({ commit }, value) => commit('clearUnreadStatus', value),
selectFriend: ({ commit }, value) => commit('selectFriend', value),
updateFriendList: ({ commit }, value) => commit('updateFriendList', value),
updateUserInfos: ({ commit }, value) => commit('updateUserInfos', value),
sendMessage: ({ commit }, msg) => commit('sendMessage', msg),
send: ({ commit }) => commit('send'),
initData: ({ commit }) => commit('initData'),
updateConversationInfo: ({ commit }, value) => commit('updateConversationInfo', value),
updateConversationIntro: ({ commit }, value) => commit('updateConversationIntro', value),
addProtoMessage: ({ commit }, value) => commit('addProtoMessage', value),
loginOut: ({ commit }, value) => commit('loginOut', value),
changetFirstLogin: ({ commit }, value) => commit('changetFirstLogin', value),
getUploadToken: ({ commit }, value) => commit('getUploadToken', value),
visibilityChange: ({ commit }, value) => commit('visibilityChange', value),
searchUser: ({ commit }, value) => commit('searchUser', value),
updateSearchUser: ({ commit }, value) => commit('updateSearchUser', value),
sendFriendAddRequest: ({ commit }, value) => commit('sendFriendAddRequest', value),
updateFriendRequest: ({ commit }, value) => commit('updateFriendRequest', value),
handleFriendRequest: ({ commit }, value) => commit('handleFriendRequest', value),
updateFriendIds: ({ commit }, value) => commit('updateFriendIds', value),
modifyMyInfo: ({ commit }, value) => commit('modifyMyInfo', value),
getUserInfos: ({ commit }, value) => commit('getUserInfos', value),
updateGroupInfos: ({ commit }, value) => commit('updateGroupInfos', value),
getGroupInfo: ({ commit }, value) => commit('getGroupInfo', value),
getGroupMember: ({ commit }, value) => commit('getGroupMember', value),
quitGroup: ({ commit }, value) => commit('quitGroup', value),
updateTempGroupMember: ({ commit }, value) => commit('updateTempGroupMember', value),
changeEmptyMessageState: ({ commit }, value) => commit('changeEmptyMessageState', value),
updateProtoMessageUid: ({ commit }, value) => commit('updateProtoMessageUid', value),
updateMessageStatus: ({ commit }, value) => commit('updateMessageStatus', value),
updateMessageContent: ({ commit }, value) => commit('updateMessageContent', value),
deleteConversation: ({ commit }, value) => commit('deleteConversation', value),
deleteMessage: ({ commit }, value) => commit('deleteMessage', value),
preAddProtoMessage: ({ commit }, value) => commit('preAddProtoMessage', value),
updateSendMessage: ({ commit }, value) => commit('updateSendMessage', value),
addOldMessage: ({ commit }, value) => commit('addOldMessage', value),
}
const store = new Vuex.Store({
state,
mutations,
getters,
actions
})
store.watch(
state => state.conversations,
value => {
LocalStore.saveConverSations(value);
},
{
deep : true
}
)
store.watch(
state => state.messages,
value => {
LocalStore.saveMessages(value);
},
{
deep : true
}
)
store.watch(
state => state.userInfoList,
value => {
LocalStore.saveUserInfoList(value);
},
{
deep : true
}
)
store.watch(
state => state.selectTarget,
value => {
LocalStore.setSelectTarget(value);
},
{
deep : true
}
)
export default store;
================================================
FILE: src/webrtc/callEndReason.js
================================================
export default class CallEndReason {
static REASON_Unknown = 'unknown';
static REASON_Busy = 'busy';
static REASON_SignalError = 'signalError';
static REASON_Hangup = 'hangup';
static REASON_MediaError = 'mediaError';
static REASON_RemoteHangup = 'remoteHangup';
static REASON_OpenCameraFailure = 'openCameraError';
static REASON_Timeout = 'timeout';
static REASON_AcceptByOtherClient = 'acceptByOtherClient';
static REASON_AllLeft = 'allLeft';
}
================================================
FILE: src/webrtc/callSession.js
================================================
import CallState from "./callState";
export default class CallSession{
callId;
clientId;
audioOnly;
startTime;
sessionCallback;
callState;
voipClient;
tos;
constructor(voipClient){
this.startTime = new Date().getTime();
this.voipClient = voipClient;
}
setState(state){
if(this.callState != state){
var previousState = this.callState;
this.callState = state;
console.log("set current call state "+this.callState);
switch(state){
case CallState.STATUS_INCOMING:
case CallState.STATUS_OUTGOING:
this.voipClient.currentEngineCallback.shouldStartRing(state === CallState.STATUS_INCOMING)
break;
case CallState.STATUS_CONNECTING:
this.voipClient.currentEngineCallback.shouldSopRing()
break;
case CallState.STATUS_IDLE:
case CallState.STATUS_CONNECTED:
if (previousState == CallState.STATUS_INCOMING || previousState == CallState.STATUS_OUTGOING) {
this.voipClient.currentEngineCallback.shouldSopRing();
}
break;
}
if(this.sessionCallback){
this.sessionCallback.didChangeState(this.callState);
}
}
}
endCall(endCallReason,sender=''){
this.setState(CallState.STATUS_IDLE);
this.voipClient.closeCall();
this.voipClient.currentSession = null;
if(this.sessionCallback){
this.sessionCallback.didCallEndWithReason(endCallReason,sender);
}
}
}
================================================
FILE: src/webrtc/callState.js
================================================
export default class CallState {
static STATUS_IDLE = 0;
static STATUS_OUTGOING = 1;
static STATUS_INCOMING = 2;
static STATUS_CONNECTING = 3;
static STATUS_CONNECTED = 4;
}
================================================
FILE: src/webrtc/engineCallback.js
================================================
export default class EngineCallback{
onReceiveCall(callSession){}
shouldStartRing(isIncomming){}
shouldSopRing(){}
}
================================================
FILE: src/webrtc/groupCallClient.js
================================================
import OnReceiverMessageListener from "../websocket/listener/onReceiverMessageListener";
import ChatManager from "../websocket/chatManager";
import CallStartMessageContent from "./message/callStartMessageContent";
import CallSession from "./callSession";
import CallState from "./callState";
import SendMessage from "../websocket/message/sendMessage";
import CallAnswerMessageContent from "./message/callAnswerMessageContent";
import CallSignalMessageContent from "./message/callSignalMessageContent";
import CallByeMessageContent from "./message/callByeMessageContent";
import CallEndReason from "./callEndReason";
import Participant from "./participant";
import kurentoUtils from "kurento-utils"
import MessageConfig from '../websocket/message/messageConfig'
import LocalStore from "../websocket/store/localstore";
export default class GroupCallClient extends OnReceiverMessageListener {
currentSession;
currentSessionCallback;
currentEngineCallback;
//是否群组聊天发起人
isInitiator;
participants = {};
constructor(store){
super();
this.store = store;
ChatManager.addReceiveMessageListener(this);
}
setCurrentSessionCallback(sessionCallback){
this.currentSessionCallback = sessionCallback;
}
setCurrentEngineCallback(engineCallback){
this.currentEngineCallback = engineCallback;
}
/**
* 开始群组音视频通话
* @param target 群组会话target
* @param tos 邀请的用户target 数组列表
* @param isAudioOnly 是否仅仅是音频聊天
*/
startCall(target,tos,isAudioOnly){
this.isInitiator = true;
//创建session
var newSession = this.newSession(target,isAudioOnly,target + new Date().getTime());
newSession.setState(CallState.STATUS_OUTGOING);
this.currentSession = newSession;
console.log("create new session "+this.currentSession.clientId+" callId "+this.currentSession.callId);
//发送callmessage
var callStartMessageContent = new CallStartMessageContent(newSession.callId,target,isAudioOnly);
this.sendMessage(target,callStartMessageContent,tos);
}
answerCall(audioOnly){
this.isInitiator = false;
console.log("isInitiator "+this.isInitiator);
this.currentSession.setState(CallState.STATUS_CONNECTING);
var answerMesage = new CallAnswerMessageContent()
answerMesage.isAudioOnly = audioOnly;
answerMesage.callId = this.currentSession.callId;
this.sendMessage(this.currentSession.clientId,answerMesage);
}
/**
* 结束群组音视频通话
*/
endCall(tos){
//发起者发送End call指令
if(this.isInitiator){
this.sendByeMessage(tos)
} else {
this.sendSignalMessage({
type: 'leaveRoom'
})
}
this.currentSession.endCall(CallEndReason.REASON_RemoteHangup,LocalStore.getUserId());
}
closeCall(){
for(var key in this.participants){
this.participants[key].dispose()
}
}
sendByeMessage(tos){
var byeMessage = new CallByeMessageContent();
this.sendMessage(this.currentSession.clientId,byeMessage,tos)
}
sendSignalMessage(msg){
var callSignalMessageContent = new CallSignalMessageContent();
//callSignalMessageContent.callId = this.currentSession.callId;
callSignalMessageContent.payload = JSON.stringify(msg);
this.sendMessage(this.currentSession.clientId,callSignalMessageContent);
}
onReceiveMessage(protoMessage){
console.log(" receive message "+protoMessage.direction)
if(new Date().getTime() - protoMessage.timestamp < 90000 && protoMessage.direction === 1 && protoMessage.conversationType == 1){
let contentClazz = MessageConfig.getMessageContentClazz(protoMessage.content.type);
if(contentClazz){
let content = new contentClazz();
try {
content.decode(protoMessage.content);
} catch(err){
console.log('decode error');
}
if(this.currentSession){
console.log("current call state "+this.currentSession.callState);
}
if(content instanceof CallStartMessageContent){
console.log("receive call startmessage");
if(this.currentSession && this.currentSession.callState !== CallState.STATUS_IDLE){
this.rejectOtherCall(content.callId,protoMessage.from);
} else {
//这里client 要指定为group的target
this.currentSession = this.newSession(protoMessage.target,content.audioOnly,content.callId);
console.log("before receive call tos "+protoMessage.tos+" from "+protoMessage.from)
//去除自己,加入对方的id
var tos = protoMessage.tos;
tos.splice(tos.findIndex(item => item === LocalStore.getUserId()), 1);
tos.push(protoMessage.from)
this.currentSession.tos = tos
console.log("after receive call tos "+this.currentSession.tos)
this.currentSession.setState(CallState.STATUS_INCOMING);
this.currentEngineCallback.onReceiveCall(this.currentSession);
}
} else if(content instanceof CallSignalMessageContent){
if(this.currentSession && this.currentSession.callState != CallState.STATUS_IDLE){
console.log("current state "+this.currentSession.callState+" call signal payload "+content.payload);
this.handleSignalMsg(content.payload);
}
} else if(content instanceof CallByeMessageContent){
if(!this.currentSession || this.currentSession.callState === CallState.STATUS_IDLE ){
return;
}
this.endCall();
}
}
}
}
async handleSignalMsg(payload){
var signalMessage = JSON.parse(payload);
var type = signalMessage.type;
console.log('message type '+type+" name "+signalMessage.name);
//新的用户来临
if(type === "newParticipantArrived"){
var name = signalMessage.name;
console.log("new participant name "+name)
this.onNewParticipant(name)
} else if(type == "existingParticipants"){
var existingParticipants = signalMessage.data;
console.log("existingParticipants "+signalMessage.data)
this.onExistingParticipants(signalMessage)
} else if(type == "participantLeft"){
var participantLeft = signalMessage.name;
console.log("participantLeft "+participantLeft)
this.onParticipantLeft(participantLeft)
} else if(type == 'iceCandidate'){
this.participants[signalMessage.name].rtcPeer.addIceCandidate(signalMessage.candidate, function (error) {
if (error) {
console.error("Error adding candidate: " + error);
return;
}
});
} else if(type == 'receiveVideoAnswer'){
this.onReceiverAnswer(signalMessage)
}
}
newSession(clientId, audioOnly, callId){
var session = new CallSession(this);
session.clientId = clientId;
session.audioOnly = audioOnly;
session.callId = callId;
session.sessionCallback = this.currentSessionCallback;
return session;
}
sendMessage(target,messageConent,tos = ''){
this.store.dispatch('sendMessage', new SendMessage(target,messageConent,tos));
}
rejectOtherCall(callId,clientId){
var byeMessage = new CallByeMessageContent();
byeMessage.callId = callId;
this.log('reject other callId '+callId +" clientId "+clientId);
this.sendMessage(clientId,byeMessage);
}
onReceiverAnswer(result){
this.participants[result.name].rtcPeer.processAnswer (result.sdpAnswer, function (error) {
if (error) return console.error (error);
});
}
onExistingParticipants(msg) {
console.log("audioOnly "+this.currentSession.audioOnly)
var constraints = {
audio : true,
video : {
mandatory : {
maxWidth : 320,
maxFrameRate : 15,
minFrameRate : 15
}
}
};
var currentUserId = LocalStore.getUserId();
var participant = new Participant(this.currentSession.clientId,currentUserId,this);
this.participants[currentUserId] = participant;
var video = participant.getVideoElement();
console.log("person video "+video.tagName)
var options = {
localVideo: video,
mediaConstraints: constraints,
onicecandidate: participant.onIceCandidate.bind(participant),
// iceServers: [{"urls":"turn:turn.liyufan.win:3478","username":"wfchat","credential":"wfchat"}]
}
var _this = this
participant.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options,
function(error) {
if(error) {
if(_this.currentSessionCallback){
_this.currentSessionCallback.didError(error)
}
return console.error(error)
}
if(_this.currentSessionCallback){
_this.currentSessionCallback.didCreateLocalVideoTrack()
}
this.generateOffer (participant.offerToReceiveVideo.bind(participant));
});
for(var sender of msg.data){
this.receiveVideo(sender)
}
}
receiveVideo(sender) {
var participant = new Participant(this.currentSession.clientId,sender,this);
this.participants[sender] = participant;
var video = participant.getVideoElement();
console.log("receiveVideo "+sender + " video "+video.tagName)
var options = {
remoteVideo: video,
onicecandidate: participant.onIceCandidate.bind(participant),
// iceServers: [{"urls":"turn:turn.liyufan.win:3478","username":"wfchat","credential":"wfchat"}]
}
var _this = this
participant.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,
function(error) {
if(error) {
return console.error(error);
}
if(_this.currentSessionCallback){
_this.currentSessionCallback.didReceiveRemoteVideoTrack(null,sender)
}
this.generateOffer (participant.offerToReceiveVideo.bind(participant));
});;
}
onNewParticipant(name){
this.receiveVideo(name)
}
onParticipantLeft(name) {
console.log('Participant ' + name + ' left');
var participant = this.participants[name];
participant.dispose();
delete this.participants[name];
this.currentSessionCallback.didCallEndWithReason(CallEndReason.REASON_Hangup,name)
}
}
================================================
FILE: src/webrtc/message/callAnswerMessageContent.js
================================================
import MessageContent from '../../websocket/message/messageContent';
import MessageContentType from '../../websocket/message/messageContentType';
import StringUtils from "../../websocket/utils/StringUtil"
export default class CallAnswerMessageContent extends MessageContent {
callId;
audioOnly;
constructor(mentionedType = 0, mentionedTargets = []) {
super(MessageContentType.VOIP_CONTENT_TYPE_ACCEPT, mentionedType, mentionedTargets);
}
digest() {
return '';
}
encode() {
let payload = super.encode();
payload.content = this.callId;
var obj;
if (this.audioOnly) {
obj = '1';
} else {
obj = '0';
}
payload.binaryContent = StringUtils.utf8_to_b64(obj);
return payload;
};
decode(payload) {
super.decode(payload);
this.callId = payload.content;
let str = StringUtils.b64_to_utf8(payload.binaryContent);
this.audioOnly = (str === '1');
}
}
================================================
FILE: src/webrtc/message/callAnswerTMessageContent.js
================================================
import MessageContent from '../../websocket/message/messageContent';
import MessageContentType from '../../websocket/message/messageContentType';
import StringUtils from "../../websocket/utils/StringUtil"
export default class CallAnswerTMessageContent extends MessageContent {
callId;
audioOnly;
constructor(mentionedType = 0, mentionedTargets = []) {
super(MessageContentType.VOIP_CONTENT_TYPE_ACCEPT_T, mentionedType, mentionedTargets);
}
digest() {
return '';
}
encode() {
let payload = super.encode();
payload.content = this.callId;
var obj;
if (this.audioOnly) {
obj = '1';
} else {
obj = '0';
}
payload.binaryContent = StringUtils.utf8_to_b64(obj);
return payload;
};
decode(payload) {
super.decode(payload);
this.callId = payload.content;
let str = StringUtils.b64_to_utf8(payload.binaryContent);
this.audioOnly = (str === '1');
}
}
================================================
FILE: src/webrtc/message/callByeMessageContent.js
================================================
import MessageContent from '../../websocket/message/messageContent';
import MessageContentType from '../../websocket/message/messageContentType';
export default class CallByeMessageContent extends MessageContent {
callId;
constructor(mentionedType = 0, mentionedTargets = []) {
super(MessageContentType.VOIP_CONTENT_TYPE_END, mentionedType, mentionedTargets);
}
digest() {
return '';
}
encode() {
let payload = super.encode();
payload.content = this.callId;
return payload;
};
decode(payload) {
super.decode(payload);
this.callId = payload.content;
}
}
================================================
FILE: src/webrtc/message/callModifyMessageContent.js
================================================
import MessageContent from '../../websocket/message/messageContent';
import MessageContentType from '../../websocket/message/messageContentType';
import StringUtils from "../../websocket/utils/StringUtil"
export default class CallModifyMessageContent extends MessageContent {
callId;
audioOnly;
constructor(mentionedType = 0, mentionedTargets = []) {
super(MessageContentType.VOIP_CONTENT_TYPE_MODIFY, mentionedType, mentionedTargets);
}
digest() {
return '';
}
encode() {
let payload = super.encode();
payload.content = this.callId;
var obj;
if (this.audioOnly) {
obj = '1';
} else {
obj = '0';
}
payload.binaryContent = StringUtils.utf8_to_b64(obj);
return payload;
};
decode(payload) {
super.decode(payload);
this.callId = payload.content;
let str = StringUtils.b64_to_utf8(payload.binaryContent);
this.audioOnly = (str === '1');
}
}
================================================
FILE: src/webrtc/message/callSignalMessageContent.js
================================================
import MessageContent from '../../websocket/message/messageContent';
import MessageContentType from '../../websocket/message/messageContentType';
import StringUtils from "../../websocket/utils/StringUtil"
export default class CallSignalMessageContent extends MessageContent {
callId;
payload;
constructor(mentionedType = 0, mentionedTargets = []) {
super(MessageContentType.VOIP_CONTENT_TYPE_SIGNAL, mentionedType, mentionedTargets);
}
digest() {
return '';
}
encode() {
let payload = super.encode();
payload.content = this.callId;
payload.binaryContent = StringUtils.utf8_to_b64(this.payload);
return payload;
};
decode(payload) {
super.decode(payload);
this.callId = payload.content;
this.payload = StringUtils.b64_to_utf8(payload.binaryContent);
}
}
================================================
FILE: src/webrtc/message/callStartMessageContent.js
================================================
import MessageContent from '../../websocket/message/messageContent';
import MessageContentType from '../../websocket/message/messageContentType';
import StringUtils from "../../websocket/utils/StringUtil"
export default class CallStartMessageContent extends MessageContent {
callId;
targetId;
connectTime;
endTime;
status;
audioOnly;
constructor(callId, targetId, audioOnly){
super(MessageContentType.VOIP_CONTENT_TYPE_START);
this.callId = callId;
this.targetId = targetId;
this.audioOnly = audioOnly;
}
digest() {
if (this.audioOnly) {
return '[语音通话]';
} else {
return '[视频通话]';
}
}
encode() {
let payload = super.encode();
payload.content = this.callId;
let obj = {
c: this.connectTime,
e: this.endTime,
s: this.status,
a: this.audioOnly ? 1 : 0,
t:this.targetId
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
};
decode(payload) {
super.decode(payload);
this.callId = payload.content;
let json = StringUtils.b64_to_utf8(payload.binaryContent);
let obj = JSON.parse(json);
this.connectTime = obj.c;
this.endTime = obj.e;
this.status = obj.s;
this.audioOnly = (obj.a === 1);
this.targetId = obj.t;
}
}
================================================
FILE: src/webrtc/participant.js
================================================
import CallSignalMessageContent from "./message/callSignalMessageContent";
export default class Participant {
target;
sender;
groupCallClient;
rtcPeer;
constructor(target,sender,groupCallClient){
this.target = target;
this.sender = sender;
this.groupCallClient = groupCallClient;
}
getVideoElement(){
return document.getElementById(this.sender);
}
onIceCandidate(candidate, wp) {
console.log("Local candidate" + JSON.stringify(candidate));
var message = {
type: 'onIceCandidate',
candidate: candidate,
name: name
};
this.groupCallClient.sendSignalMessage(message);
}
offerToReceiveVideo(error, offerSdp, wp){
if (error) return console.error ("sdp offer error"+error)
console.log(this.sender + ' Invoking SDP offer callback function');
var msg = {
type : "receiveVideoFrom",
sender : this.sender,
sdpOffer : offerSdp
};
this.groupCallClient.sendSignalMessage(msg);
}
dispose() {
console.log('Disposing participant ' + this.sender);
this.rtcPeer.dispose();
}
// sendSignalMessage(msg){
// var callSignalMessageContent = new CallSignalMessageContent();
// //callSignalMessageContent.callId = this.currentSession.callId;
// callSignalMessageContent.payload = JSON.stringify(msg);
// this.groupCallClient.sendMessage(this.target,callSignalMessageContent);
// }
}
================================================
FILE: src/webrtc/sessionCallback.js
================================================
export default class SessionCallback{
didCallEndWithReason(callEndReason,sender = ''){}
didChangeState(callState){}
didChangeMode(mode){}
didCreateLocalVideoTrack(stream){}
didReceiveRemoteVideoTrack(stream,sender = ''){}
didReceiveRemoteAudioTrack(stream){}
didError(error){}
didGetStats(stats){}
}
================================================
FILE: src/webrtc/voipclient.js
================================================
import CallStartMessageContent from "./message/callStartMessageContent";
import ChatManager from "../websocket/chatManager";
import OnReceiverMessageListener from "../websocket/listener/onReceiverMessageListener";
import MessageConfig from '../websocket/message/messageConfig'
import CallAnswerMessageContent from "./message/callAnswerMessageContent";
import CallSignalMessageContent from "./message/callSignalMessageContent";
import CallAnswerTMessageContent from "./message/callAnswerTMessageContent";
import CallSession from "./callSession";
import CallByeMessageContent from "./message/callByeMessageContent";
import SendMessage from "../websocket/message/sendMessage";
import CallState from "./callState";
import CallEndReason from "./callEndReason";
export default class VoipClient extends OnReceiverMessageListener{
myPeerConnection;
webcamStream;
mediaConstraints = {
audio: true, // We want an audio track
video: true
};
sender;
//是否为createOff发起人
isInitiator;
//当前会话
currentSession;
currentSessionCallback;
currentEngineCallback;
constructor(store){
super();
this.store = store;
ChatManager.addReceiveMessageListener(this);
}
setCurrentSessionCallback(sessionCallback){
this.currentSessionCallback = sessionCallback;
}
setCurrentEngineCallback(engineCallback){
this.currentEngineCallback = engineCallback;
}
startCall(target,isAudioOnly){
this.isInitiator = false;
//创建session
var newSession = this.newSession(target,isAudioOnly,target + new Date().getTime());
newSession.setState(CallState.STATUS_OUTGOING);
this.currentSession = newSession;
console.log("create new session "+this.currentSession.clientId+" callId "+this.currentSession.callId);
//发送callmessage
var callStartMessageContent = new CallStartMessageContent(newSession.callId,target,isAudioOnly);
this.offerMessage(target,callStartMessageContent);
//如果时视频,启动预览
this.startPreview();
}
cancelCall(){
var byeMessage = new CallByeMessageContent();
byeMessage.callId = this.currentSession.callId;
this.offerMessage(this.currentSession.clientId,byeMessage);
console.log("send bye message");
this.currentSession.endCall(CallEndReason.REASON_RemoteHangup);
this.currentSession = null;
}
answerCall(audioOnly){
this.isInitiator = true;
console.log("isInitiator "+this.isInitiator);
this.currentSession.setState(CallState.STATUS_CONNECTING);
var answerTMesage = new CallAnswerTMessageContent()
answerTMesage.isAudioOnly = audioOnly;
answerTMesage.callId = this.currentSession.callId;
this.offerMessage(this.currentSession.clientId,answerTMesage);
this.startPreview();
}
newSession(clientId, audioOnly, callId){
var session = new CallSession(this);
session.clientId = clientId;
session.audioOnly = audioOnly;
session.callId = callId;
session.sessionCallback = this.currentSessionCallback;
return session;
}
rejectOtherCall(callId,clientId){
var byeMessage = new CallByeMessageContent();
byeMessage.callId = callId;
this.log('reject other callId '+callId +" clientId "+clientId);
this.offerMessage(clientId,byeMessage);
}
offerMessage(target,messageConent){
this.store.dispatch('sendMessage', new SendMessage(target,messageConent));
}
offerMessageByType(type){
var callSignalMessageContent = new CallSignalMessageContent();
callSignalMessageContent.callId = this.currentSession.callId;
var jsonPayload = {
type: type,
sdp: this.myPeerConnection.localDescription.sdp
}
callSignalMessageContent.payload = JSON.stringify(jsonPayload);
this.offerMessage(this.currentSession.clientId,callSignalMessageContent);
}
/**
* 接收信令服务传递过来的消息
*/
onReceiveMessage(protoMessage){
//只处理接收消息,对于同一用户不同session会话忽略
if(new Date().getTime() - protoMessage.timestamp < 90000 && protoMessage.direction === 1 && protoMessage.conversationType == 0){
let contentClazz = MessageConfig.getMessageContentClazz(protoMessage.content.type);
if(contentClazz){
let content = new contentClazz();
try {
content.decode(protoMessage.content);
} catch(err){
console.log('decode error');
}
if(this.currentSession){
console.log("current call state "+this.currentSession.callState);
}
if(content instanceof CallStartMessageContent){
console.log("receive call startmessage");
if(this.currentSession && this.currentSession.callState !== CallState.STATUS_IDLE){
this.rejectOtherCall(content.callId,protoMessage.from);
} else {
this.currentSession = this.newSession(protoMessage.from,content.audioOnly,content.callId);
this.currentSession.setState(CallState.STATUS_INCOMING);
this.currentEngineCallback.onReceiveCall(this.currentSession);
}
} else if(content instanceof CallAnswerMessageContent || content instanceof CallAnswerTMessageContent){
this.isInitiator = false;
if(this.currentSession && this.currentSession.callState != CallState.STATUS_IDLE){
console.log(" CallAnswerMessageContent callState "+this.currentSession.callState);
if(protoMessage.from === this.currentSession.clientId && content.callId === this.currentSession.callId){
if(this.currentSession.callState != CallState.STATUS_OUTGOING){
// this.rejectOtherCall(this.currentSession.callId,this.currentSession.clientId);
} else if(this.currentSession.callState === CallState.STATUS_OUTGOING){
this.currentSession.setState(CallState.STATUS_CONNECTING);
}
}
}
} else if(content instanceof CallSignalMessageContent){
if(this.currentSession && this.currentSession.callState != CallState.STATUS_IDLE){
console.log("current state "+this.currentSession.callState+" call signal payload "+content.payload);
if(this.currentSession.callState === CallState.STATUS_CONNECTING || this.currentSession.callState === CallState.STATUS_CONNECTED){
if(protoMessage.from === this.currentSession.clientId && content.callId === this.currentSession.callId){
this.handleSignalMsg(content.payload);
}
} else {
this.currentSession.endCall(CallEndReason.REASON_AcceptByOtherClient);
// this.currentSession.sessionCallback.didCallEndWithReason(CallEndReason.REASON_AcceptByOtherClient);
}
}
} else if(content instanceof CallByeMessageContent){
if(!this.currentSession || this.currentSession.callState === CallState.STATUS_IDLE || protoMessage.from != this.currentSession.clientId || content.callId != this.currentSession.callId){
return;
}
this.cancelCall();
}
}
}
}
async handleSignalMsg(payload){
var signalMessage = JSON.parse(payload);
var type = signalMessage.type;
console.log('message type '+type);
if(type === "candidate"){
var rTCIceCandidateInit = {
candidate: signalMessage.candidate,
sdpMLineIndex: signalMessage.label,
sdpMid: signalMessage.id
}
var candidate = new RTCIceCandidate(rTCIceCandidateInit);
this.log("*** Adding received ICE candidate: " + JSON.stringify(candidate));
try {
await this.myPeerConnection.addIceCandidate(candidate);
} catch(err){
this.reportError(err);
}
} else if(type === 'remove-candidates'){
this.log("remove candidates ");
} else {
var desc = new RTCSessionDescription(signalMessage);
if(type === 'answer'){
await this.myPeerConnection.setRemoteDescription(desc);
} else if(type === 'offer'){
await this.myPeerConnection.setRemoteDescription(desc);
await this.myPeerConnection.setLocalDescription(await this.myPeerConnection.createAnswer());
//send answer message
var callSignalMessageContent = new CallSignalMessageContent();
callSignalMessageContent.callId = this.currentSession.callId;
var jsonPayload = {
type: 'answer',
sdp: this.myPeerConnection.localDescription.sdp
}
callSignalMessageContent.payload = JSON.stringify(jsonPayload);
this.offerMessage(this.currentSession.clientId,callSignalMessageContent);
}
}
}
async handleOfferMessage(){
console.log("*** Negotiation needed "+this.isInitiator);
if(!this.isInitiator){
return;
}
try {
console.log("---> Creating offer");
const offer = await this.myPeerConnection.createOffer();
// If the connection hasn't yet achieved the "stable" state,
// return to the caller. Another negotiationneeded event
// will be fired when the state stabilizes.
if (this.myPeerConnection.signalingState != "stable") {
console.log(" -- The connection isn't stable yet; postponing...")
return;
}
// Establish the offer as the local peer's current
// description.
console.log("---> Setting local description to the offer");
await this.myPeerConnection.setLocalDescription(offer);
// Send the offer to the remote peer.
console.log("---> Sending the offer to the remote peer");
this.offerMessageByType('offer');
} catch(err) {
console.log("*** The following error occurred while handling the negotiationneeded event:");
this.reportError(err);
};
}
async startPreview(){
this.log("Starting to prepare an invitation");
if (this.myPeerConnection) {
alert("You can't start a call because you already have one open!");
} else {
// Get access to the webcam stream and attach it to the
// "preview" box (id "local_video").
try {
this.mediaConstraints = {
audio: true, // We want an audio track
video: !this.currentSession.isAudioOnly
}
this.webcamStream = await navigator.mediaDevices.getUserMedia(this.mediaConstraints);
if(!this.currentSession.isAudioOnly){
this.currentSessionCallback.didCreateLocalVideoTrack(this.webcamStream);
}
} catch(err) {
this.handleGetUserMediaError(err);
return;
}
// Call createPeerConnection() to create the RTCPeerConnection.
// When this returns, myPeerConnection is our RTCPeerConnection
// and webcamStream is a stream coming from the camera. They are
// not linked together in any way yet.
this.createPeerConnection();
// Add the tracks from the stream to the RTCPeerConnection
try {
this.webcamStream.getTracks().forEach(
track => this.sender = this.myPeerConnection.addTrack(track, this.webcamStream)
);
} catch(err) {
this.handleGetUserMediaError(err);
}
}
}
async createPeerConnection() {
this.log("Setting up a connection...");
// Create an RTCPeerConnection which knows to use our chosen
// STUN server.
this.myPeerConnection = new RTCPeerConnection({
iceServers: [ // Information about ICE servers - Use your own!
{
urls: "turn:turn.fsharechat.cn:3478", // A TURN server
username: "comsince",
credential: "comsince"
}
]
});
// Set up event handlers for the ICE negotiation process.
this.myPeerConnection.onicecandidate = this.handleICECandidateEvent;
this.myPeerConnection.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent;
this.myPeerConnection.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent;
this.myPeerConnection.onsignalingstatechange = this.handleSignalingStateChangeEvent;
this.myPeerConnection.onnegotiationneeded = this.handleNegotiationNeededEvent;
this.myPeerConnection.ontrack = this.handleTrackEvent;
}
// Called by the WebRTC layer to let us know when it's time to
// begin, resume, or restart ICE negotiation.
handleNegotiationNeededEvent = () => {
this.handleOfferMessage();
}
// Handles |icecandidate| events by forwarding the specified
// ICE candidate (created by our local ICE agent) to the other
// peer through the signaling server.
//接收来自信令服务器发送来的ICE candidate事件消息
handleICECandidateEvent = (event) => {
if (event.candidate) {
console.log("*** Outgoing ICE candidate: " + event.candidate.candidate);
var candidateMessageContent = new CallSignalMessageContent();
candidateMessageContent.callId = this.currentSession.callId;
var candidatePayload = {
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
}
candidateMessageContent.payload = JSON.stringify(candidatePayload);
this.offerMessage(this.currentSession.clientId,candidateMessageContent);
}
}
// Handle |iceconnectionstatechange| events. This will detect
// when the ICE connection is closed, failed, or disconnected.
//
// This is called when the state of the ICE agent changes.
handleICEConnectionStateChangeEvent = (event) => {
console.log("*** ICE connection state changed to " + this.myPeerConnection.iceConnectionState);
switch(this.myPeerConnection.iceConnectionState) {
case "connected":
this.currentSession.setState(CallState.STATUS_CONNECTED);
break;
case "closed":
case "failed":
case "disconnected":
this.cancelCall();
break;
}
}
// Set up a |signalingstatechange| event handler. This will detect when
// the signaling connection is closed.
//
// NOTE: This will actually move to the new RTCPeerConnectionState enum
// returned in the property RTCPeerConnection.connectionState when
// browsers catch up with the latest version of the specification!
handleSignalingStateChangeEvent = (event) => {
console.log("*** WebRTC signaling state changed to: " + this.myPeerConnection.signalingState);
switch(this.myPeerConnection.signalingState) {
case "closed":
this.cancelCall();
break;
}
}
// Called by the WebRTC layer when events occur on the media tracks
// on our WebRTC call. This includes when streams are added to and
// removed from the call.
//
// track events include the following fields:
//
// RTCRtpReceiver receiver
// MediaStreamTrack track
// MediaStream[] streams
// RTCRtpTransceiver transceiver
//
// In our case, we're just taking the first stream found and attaching
// it to the element for incoming media.
handleTrackEvent = (event) => {
console.log("comming stream");
if(this.currentSession.isAudioOnly){
this.currentSessionCallback.didReceiveRemoteAudioTrack(event.streams[0]);
} else {
this.currentSessionCallback.didReceiveRemoteVideoTrack(event.streams[0]);
}
}
handleICEGatheringStateChangeEvent = (event) => {
console.log("*** ICE gathering state changed to: " + event);
}
// Close the RTCPeerConnection and reset variables so that the user can
// make or receive another call if they wish. This is called both
// when the user hangs up, the other user hangs up, or if a connection
// failure is detected.
closeCall() {
this.log("Closing the call");
// Close the RTCPeerConnection
if (this.myPeerConnection) {
this.log("--> Closing the peer connection");
// Disconnect all our event listeners; we don't want stray events
// to interfere with the hangup while it's ongoing.
this.myPeerConnection.ontrack = null;
this.myPeerConnection.onnicecandidate = null;
this.myPeerConnection.oniceconnectionstatechange = null;
this.myPeerConnection.onsignalingstatechange = null;
this.myPeerConnection.onicegatheringstatechange = null;
this.myPeerConnection.onnotificationneeded = null;
// Stop all transceivers on the connection
// this.myPeerConnection.getTransceivers().forEach(transceiver => {
// transceiver.stop();
// });
this.myPeerConnection.removeTrack(this.sender);
// Stop the webcam preview as well by pausing the
// element, then stopping each of the getUserMedia() tracks
// on it.
if(!this.currentSession.isAudioOnly){
var localVideo = document.getElementById("wxCallLocalVideo");
if (localVideo.srcObject) {
localVideo.pause();
localVideo.srcObject.getTracks().forEach(track => {
track.stop();
});
}
} else {
this.webcamStream.getTracks.forEach(
track => {
track.stop();
}
)
}
// Close the peer connection
this.myPeerConnection.close();
this.myPeerConnection = null;
this.webcamStream = null;
}
// Disable the hangup button
// document.getElementById("hangup-button").disabled = true;
// targetUsername = null;
}
// Handle errors which occur when trying to access the local media
// hardware; that is, exceptions thrown by getUserMedia(). The two most
// likely scenarios are that the user has no camera and/or microphone
// or that they declined to share their equipment when prompted. If
// they simply opted not to share their media, that's not really an
// error, so we won't present a message in that situation.
handleGetUserMediaError(e) {
this.log_error(e);
switch(e.name) {
case "NotFoundError":
alert("Unable to open your call because no camera and/or microphone" +
"were found.");
break;
case "SecurityError":
case "PermissionDeniedError":
// Do nothing; this is the same as the user canceling the call.
break;
default:
alert("Error opening your camera and/or microphone: " + e.message);
break;
}
this.cancelCall();
}
reportError(errMessage) {
this.log_error(`Error ${errMessage.name}: ${errMessage.message}`);
}
log_error(text) {
var time = new Date();
console.trace("[" + time.toLocaleTimeString() + "] " + text);
}
log(text) {
var time = new Date();
console.log("[" + time.toLocaleTimeString() + "] " + text);
}
}
================================================
FILE: src/websocket/chatManager.js
================================================
export default class ChatManager {
static onReceiveMessageListeners = [];
static addReceiveMessageListener(listener){
this.onReceiveMessageListeners.push(listener);
}
static onReceiveMessage(protoMessage){
for(var messageListener of this.onReceiveMessageListeners){
messageListener.onReceiveMessage(protoMessage);
}
}
static removeOnReceiveMessageListener(){
ChatManager.onReceiveMessageListeners = [];
}
}
================================================
FILE: src/websocket/future/futureResult.js
================================================
export default class FutureResult {
code;
result;
constructor(code, result){
this.code = code
this.result = result
}
}
================================================
FILE: src/websocket/future/promiseResolve.js
================================================
export default class PromiseResolve {
resolve;
timeoutId;
protoMessageId;
constructor(resolve,timeoutId){
this.resolve = resolve;
this.timeoutId = timeoutId;
}
}
================================================
FILE: src/websocket/handler/abstractmessagehandler.js
================================================
import MessageHandler from "./messageHandler";
import Logger from "../utils/logger";
import FutureResult from '../future/futureResult';
import { SUCCESS_CODE, ERROR_CODE } from "../../constant";
/**
* 关于promise返回说明
* 1.对于操作型消息,一般只需要返回成功与失败即可
* 2.对于结果型消息,一般判断是否为空
*/
export default class AbstractMessageHandler extends MessageHandler{
constructor(vueWebsocket){
super();
this.vueWebsocket = vueWebsocket;
}
get vueWebsocketClient(){
return this.vueWebsocket;
}
processMessage(proto){
Logger.log("AbstractMessageHandler messageId "+proto.messageId +" proto content "+proto.content);
var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId);
if(promiseReslove){
clearTimeout(promiseReslove.timeoutId);
if(proto.content == ''){
promiseReslove.resolve(new FutureResult(ERROR_CODE,proto.content));
} else {
promiseReslove.resolve(new FutureResult(SUCCESS_CODE,this.notifyContent(proto.content)));
}
this.vueWebsocket.resolvePromiseMap.delete(proto.messageId);
}
}
notifyContent(content){
return content
}
}
================================================
FILE: src/websocket/handler/addGroupMemberHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GAM } from "../../constant";
export default class AddGroupMemberHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GAM;
}
}
================================================
FILE: src/websocket/handler/connectackhandler.js
================================================
import { CONNECT_ACK } from "../../constant";
import AbstractMessageHandler from "./abstractmessagehandler";
import LocalStore from "../store/localstore";
export default class ConnectAckHandler extends AbstractMessageHandler{
constructor(vueWebsocket){
super(vueWebsocket);
}
match(protoObj){
return protoObj.signal == CONNECT_ACK;
}
processMessage(data){
console.log("ConnectAckHandler process message");
var connectAcceptedMessage = JSON.parse(data.content);
console.log("connectAcceptedMessage friendHead "+connectAcceptedMessage.friendHead+" messageHead "+connectAcceptedMessage.messageHead);
//拉取朋友列表
this.vueWebsocket.getFriend();
let lastMessageSeq = LocalStore.getLastMessageSeq();
if(!lastMessageSeq){
lastMessageSeq = '0';
this.vueWebsocket.sendAction('changetFirstLogin',true);
} else {
this.vueWebsocket.sendAction('changetFirstLogin',false);
}
//好友请求信息
this.vueWebsocket.getFriendRequest(LocalStore.getFriendRequestVersion());
//初始拉取消息列表
this.vueWebsocket.pullMessage(lastMessageSeq,0,0,LocalStore.getSendMessageCount());
LocalStore.setLastMessageSeq(connectAcceptedMessage.messageHead);
}
}
================================================
FILE: src/websocket/handler/createGroupHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GC, ERROR_CODE, SUCCESS_CODE } from "../../constant";
export default class CreateGroupHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GC;
}
}
================================================
FILE: src/websocket/handler/dismissGroupHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GD } from "../../constant";
export default class DismissGroupHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GD;
}
}
================================================
FILE: src/websocket/handler/friendAddRequestHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { FAR,PUB_ACK } from "../../constant";
import Logger from "../utils/logger";
export default class FriendAddRequestHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == FAR;
}
processMessage(proto){
var result = JSON.parse(proto.content);
Logger.log("friend add request result "+result);
}
}
================================================
FILE: src/websocket/handler/friendRequestHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, FRP } from "../../constant";
import LocalStore from "../store/localstore";
export default class FriendRequestHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == FRP;
}
processMessage(proto){
if(proto.content != null && proto.content != ''){
var friendRequests = JSON.parse(proto.content);
var validRequest = [];
var targeIds = [];
for(var friendRequest of friendRequests){
if(friendRequest.from != LocalStore.getUserId()){
validRequest.push(friendRequest);
targeIds.push(friendRequest.from);
}
}
this.vueWebsocket.getUserInfos(targeIds);
this.vueWebsocket.sendAction("updateFriendRequest",validRequest);
}
}
}
================================================
FILE: src/websocket/handler/getGroupInfoHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { GPGI, PUB_ACK } from "../../constant";
import GroupInfo from "../model/groupInfo";
export default class GetGroupInfoHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GPGI;
}
processMessage(proto){
if(proto.content != null){
var groupInfoList = JSON.parse(proto.content);
var groups = [];
for(var groupInfo of groupInfoList){
groups.push(GroupInfo.convert2GroupInfo(groupInfo));
}
this.vueWebsocket.sendAction("updateGroupInfos",groups);
}
}
}
================================================
FILE: src/websocket/handler/getGroupMemberHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GPGM } from "../../constant";
import GroupMember from "../model/groupMember";
export default class GetGroupMemberHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GPGM;
}
notifyContent(content){
var groupMemberList = JSON.parse(content);
var groupMembers = [];
for(var groupMember of groupMemberList){
groupMembers.push(GroupMember.convert2GroupMember(groupMember));
}
return groupMembers;
}
}
================================================
FILE: src/websocket/handler/getMinioUploadUrlHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GMURL } from "../../constant";
export default class GetMinioUploadUrlHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GMURL;
}
notifyContent(content){
var result = JSON.parse(content);
return result;
}
}
================================================
FILE: src/websocket/handler/getUploadtokenHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GQNUT } from "../../constant";
import LocalStore from "../store/localstore";
export default class UploadTokenHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GQNUT;
}
processMessage(proto){
if(proto.content){
var uploadTokenResponse = JSON.parse(proto.content);
var domain = uploadTokenResponse.domain;
var token = uploadTokenResponse.token;
console.log("domain "+domain+" token "+token);
LocalStore.setUploadToken(domain,token);
}
}
}
================================================
FILE: src/websocket/handler/getfriendresultHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, FP } from "../../constant";
import LocalStore from "../store/localstore";
export default class GetFriendResultHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == FP;
}
processMessage(proto){
var friendList = JSON.parse(proto.content);
var userIds = [];
for(var i in friendList){
userIds[i] = friendList[i].friendUid;
}
this.vueWebsocket.sendAction("updateFriendIds",friendList);
//获取当前登录用户信息
userIds.push(LocalStore.getUserId());
this.vueWebsocket.getUserInfos(userIds);
}
}
================================================
FILE: src/websocket/handler/getuserinfoHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, UPUI, ERROR_CODE, SUCCESS_CODE } from "../../constant";
import UserInfo from "../model/userInfo";
import py from "pinyin"
import { isBuffer } from "util";
import FutureResult from "../future/futureResult";
export default class GetUserInfoHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == UPUI;
}
processMessage(proto){
if(proto.content != null && proto.content != ''){
var userInfoList = JSON.parse(proto.content);
var stateFriendList = [];
var userInfos = [];
for(var i in userInfoList){
var displayName = userInfoList[i].displayName === '' ? userInfoList[i].mobile : userInfoList[i].displayName;
var pinyinInitial = py(displayName,{
style: py.STYLE_FIRST_LETTER
});
var initial = pinyinInitial[0][0];
if(initial.length > 1){
var initial = initial.substr(0,1);
var reg= /^[A-Za-z]/;
if(reg.test(initial)){
initial = initial.toUpperCase();
} else {
initial = "#";
}
} else {
initial = initial.toUpperCase();
}
stateFriendList.push({
id: parseInt(i) + 1,
wxid: userInfoList[i].uid, //微信号
initial: initial, //姓名首字母
img: userInfoList[i].portrait, //头像
signature: "", //个性签名
nickname: displayName, //昵称
sex: 0, //性别 1为男,0为女
remark: displayName, //备注
area: userInfoList[i].address, //地区
});
userInfos.push(UserInfo.convert2UserInfo(userInfoList[i]));
}
if(this.vueWebsocket.resolvePromiseMap.has(proto.messageId)){
console.log("messageId "+proto.messageId);
var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId);
clearTimeout(promiseReslove.timeoutId);
var displayName = userInfoList[0].displayName === '' ? userInfoList[0].mobile : userInfoList[0].displayName;
promiseReslove.resolve(new FutureResult(SUCCESS_CODE,displayName));
this.vueWebsocket.resolvePromiseMap.delete(proto.messageId);
}
this.vueWebsocket.sendAction("updateUserInfos",userInfos);
this.vueWebsocket.sendAction("updateFriendList",stateFriendList);
}
}
}
================================================
FILE: src/websocket/handler/handleFriendRequestHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, FHR } from "../../constant";
import Logger from "../utils/logger";
export default class HandleFriendRequestHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == FHR;
}
processMessage(proto){
if(proto.content && proto.content != ''){
var message = JSON.parse(proto.content);
Logger.log("handle friend request "+message);
}
}
}
================================================
FILE: src/websocket/handler/kickGroupmemberHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GKM } from "../../constant";
export default class KickGroupMemberHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GKM;
}
}
================================================
FILE: src/websocket/handler/loadRemoteMessageHander.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, LRM } from "../../constant";
export default class LoadRemoteMessageHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == LRM;
}
}
================================================
FILE: src/websocket/handler/messageHandler.js
================================================
export default class MessageHandler{
match(signal){
return false;
}
processMessage(data){
}
}
================================================
FILE: src/websocket/handler/modifyMyInfoHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { MMI, PUB_ACK } from "../../constant";
import Logger from "../utils/logger";
export default class ModifyInfoHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == MMI;
}
processMessage(proto){
if(proto && proto.content != ''){
Logger.log("modify myInfo "+proto.content);
}
}
}
================================================
FILE: src/websocket/handler/notifyFriendHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { FN, PUBLISH } from "../../constant";
import Logger from "../utils/logger";
export default class NotifyFriendHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUBLISH && proto.subSignal == FN;
}
processMessage(proto){
if(proto.content && proto.content != ''){
var friendNotify = JSON.parse(proto.content);
Logger.log("friend head "+friendNotify.head);
this.vueWebsocket.getFriend();
}
}
}
================================================
FILE: src/websocket/handler/notifyFriendRequestHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUBLISH, FRN } from "../../constant";
import Logger from "../utils/logger";
import LocalStore from "../store/localstore";
export default class NotifyFriendRequestHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUBLISH && proto.subSignal == FRN;
}
processMessage(proto){
if(proto.content != null){
var friendRequestNotification = JSON.parse(proto.content);
Logger.log("friend request version "+friendRequestNotification.version);
LocalStore.setFriendRequestVersion(friendRequestNotification.version);
this.vueWebsocket.getFriendRequest(friendRequestNotification.version);
}
}
}
================================================
FILE: src/websocket/handler/notifyMessageHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUBLISH, MN } from "../../constant";
import LocalStore from "../store/localstore";
export default class NotifyMessageHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUBLISH && proto.subSignal == MN;
}
processMessage(proto){
console.log("notifymessage "+proto.content);
let notify = JSON.parse(proto.content);
console.log("notify messageHead "+notify.messageHead+" notify type "+notify.type);
//更新messageHead
LocalStore.resetSendMessageCount();
LocalStore.setLastMessageSeq(notify.messageHead);
this.vueWebsocket.pullMessage(notify.messageHead,notify.type,1);
}
}
================================================
FILE: src/websocket/handler/notifyRecallMessageHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUBLISH, RMN } from "../../constant";
export default class NotifyRecallMessageHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUBLISH && proto.subSignal == RMN;
}
processMessage(proto){
if(proto.content){
var notifyRecalMessage = JSON.parse(proto.content);
console.log("from user "+notifyRecalMessage.fromUser + " messageUid "+notifyRecalMessage.messageUid);
this.vueWebsocket.sendAction('updateMessageContent',notifyRecalMessage);
}
}
}
================================================
FILE: src/websocket/handler/quitGroupHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, GQ } from "../../constant";
export default class QuitGroupHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == GQ;
}
}
================================================
FILE: src/websocket/handler/recallMessageHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, MR } from "../../constant";
export default class RecallMessageHandler extends AbstractMessageHandler {
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == MR;
}
notifyContent(content){
var content = JSON.parse(content);
return content.code;
}
}
================================================
FILE: src/websocket/handler/receiveMessageHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { MP, PUB_ACK } from "../../constant";
import Message from "../message/message";
import ProtoMessage from "../message/protomessage";
import ProtoConversationInfo from '../model/protoConversationInfo'
import LocalStore from "../store/localstore";
import UnreadCount from "../model/unReadCount";
import MessageConfig from "../message/messageConfig";
import PersistFlag from "../message/persistFlag";
import ChatManager from "../chatManager";
export default class ReceiveMessageHandler extends AbstractMessageHandler {
conversations = [];
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == MP;
}
processMessage(proto){
LocalStore.resetSendMessageCount();
var content = JSON.parse(proto.content);
console.log("current "+content.current+" head "+content.head+" messageCount "+content.messageCount);
if(content.messageCount == 0){
this.vueWebsocket.sendAction('changeEmptyMessageState',true);
} else {
this.vueWebsocket.sendAction('changeEmptyMessageState',false);
}
for(var protoMessage of content.messageResponseList){
var protoMessage = ProtoMessage.toProtoMessage(protoMessage);
if(MessageConfig.isDisplayableMessage(protoMessage)){
this.addProtoMessage(protoMessage);
}
ChatManager.onReceiveMessage(protoMessage);
}
this.vueWebsocket.sendAction('changetFirstLogin',false);
}
addProtoMessage(protoMessage){
this.vueWebsocket.sendAction('addProtoMessage',protoMessage);
}
}
================================================
FILE: src/websocket/handler/searchUserResultHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { US,PUB_ACK } from "../../constant";
import Logger from "../utils/logger";
import UserInfo from "../model/userInfo";
export default class SearchUserResultHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == US;
}
processMessage(proto){
var searchUserList = JSON.parse(proto.content);
this.vueWebsocket.sendAction("updateSearchUser",searchUserList);
}
}
================================================
FILE: src/websocket/handler/sendMessageHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { MS, PUB_ACK } from "../../constant";
import LocalStore from "../store/localstore";
import Logger from "../utils/logger";
import MessageStatus from "../message/messageStatus";
export default class SendMessageHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == MS;
}
processMessage(proto){
Logger.log("sendMessage Handler messageId "+proto.messageId)
if(this.vueWebsocket.resolvePromiseMap.has(proto.messageId)){
var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId);
if(proto.content != ''){
var content = JSON.parse(proto.content);
console.log("SendMessageHandler messageId "+content.messageId+" timestamp protoMessageId "+promiseReslove.protoMessageId);
this.vueWebsocket.sendAction("updateProtoMessageUid",{
messageId: promiseReslove.protoMessageId,
messageUid: content.messageId
});
//update messageStatus
this.vueWebsocket.sendAction("updateMessageStatus",{
messageId: promiseReslove.protoMessageId,
status: MessageStatus.Sent
});
LocalStore.updateSendMessageCount();
promiseReslove.resolve('success');
} else {
this.vueWebsocket.sendAction("updateMessageStatus",{
messageId: promiseReslove.protoMessageId,
status: MessageStatus.SendFailure
});
promiseReslove.resolve('fail');
}
clearTimeout(promiseReslove.timeoutId);
this.vueWebsocket.resolvePromiseMap.delete(proto.messageId);
}
}
}
================================================
FILE: src/websocket/handler/setFriendAliasRequestHandler.js
================================================
import AbstractMessageHandler from "./abstractmessagehandler";
import { PUB_ACK, FALS } from "../../constant";
export default class SetFriendAliasRequestHandler extends AbstractMessageHandler{
match(proto){
return proto.signal == PUB_ACK && proto.subSignal == FALS;
}
}
================================================
FILE: src/websocket/index.js
================================================
import {PUBLISH, FP, UPUI, MP, MS, KEY_VUE_USER_ID, KEY_VUE_DEVICE_ID, DISCONNECT, GPGI, GQNUT, US, FAR, FRP, FHR, MMI, GPGM, GC, GQ, PUB_ACK, ERROR_CODE, MR, GAM, GMI, GKM, GD, GMURL, FALS, LRM} from '../constant'
import {decrypt,encrypt} from './utils/aes'
import {CONNECT} from '../constant'
import {WebSocketProtoMessage} from './message/websocketprotomessage'
import ConnectAckHandler from './handler/connectackhandler';
import GetFriendResultHandler from './handler/getfriendresultHandler';
import GetUserInfoHandler from './handler/getuserinfoHandler';
import ReceiveMessageHandler from './handler/receiveMessageHandler';
import NotifyMessageHandler from './handler/notifyMessageHandler';
import GetGroupInfoHandler from './handler/getGroupInfoHandler';
import SendMessageHandler from './handler/sendMessageHandler';
import UploadTokenHandler from './handler/getUploadtokenHandler'
import LocalStore from './store/localstore';
import SearchUserResultHandler from './handler/searchUserResultHandler';
import FriendAddRequestHandler from './handler/friendAddRequestHandler';
import NotifyFriendRequestHandler from './handler/notifyFriendRequestHandler';
import FriendRequestHandler from './handler/friendRequestHandler';
import HandleFriendRequestHandler from './handler/handleFriendRequestHandler';
import NotifyFriendHandler from './handler/notifyFriendHandler';
import ModifyInfoHandler from './handler/modifyMyInfoHandler';
import GetGroupMemberHandler from './handler/getGroupMemberHandler';
import PromiseResolve from './future/promiseResolve';
import {WS_PROTOCOL,WS_IP,WS_PORT,HEART_BEAT_INTERVAL,RECONNECT_INTERVAL,BINTRAY_TYPE} from '../constant/index'
import vuexStore from '../store'
import Logger from './utils/logger';
import GroupInfo from './model/groupInfo';
import GroupType from './model/groupType';
import GroupMember from './model/groupMember';
import GroupMemberType from './model/groupMemberType';
import CreateGroupHandler from './handler/createGroupHandler';
import QuitGroupHandler from './handler/quitGroupHandler';
import { fail } from 'assert';
import FutureResult from './future/futureResult';
import RecallMessageHandler from './handler/recallMessageHandler';
import NotifyRecallMessageHandler from './handler/notifyRecallMessageHandler';
import AddGroupMemberHandler from './handler/addGroupMemberHandler';
import KickGroupMemberHandler from './handler/kickGroupmemberHandler'
import DismissGroupHandler from './handler/dismissGroupHandler';
import GetMinioUploadUrlHandler from './handler/getMinioUploadUrlHandler';
import SetFriendAliasRequestHandler from './handler/setFriendAliasRequestHandler';
import LoadRemoteMessageHandler from './handler/loadRemoteMessageHander';
export default class VueWebSocket {
handlerList = [];
userDisconnect = false;
isconnected = false;
resolvePromiseMap = new Map();
// constructor(ws_protocol,ip,port,heartbeatTimeout,reconnectInterval,binaryType,vuexStore){
// this.ws_protocol = ws_protocol;
// this.ip= ip;
// this.port = port;
// this.heartbeatTimeout = heartbeatTimeout;
// this.reconnectInterval = reconnectInterval;
// this.binaryType = binaryType;
// this.url = ws_protocol + '://' + ip + ':'+ port;
// this.vuexStore = vuexStore;
// this.initHandlerList();
// }
constructor(){
this.ws_protocol = WS_PROTOCOL;
this.ip= WS_IP;
this.port = WS_PORT;
this.heartbeatTimeout = HEART_BEAT_INTERVAL;
this.reconnectInterval = RECONNECT_INTERVAL;
this.binaryType = BINTRAY_TYPE;
this.url = WS_PROTOCOL + '://' + WS_IP ;
this.initHandlerList();
this.connect(true);
}
// constructor(ws_protocol,ip,port,heartbeatTimeout,reconnectInterval,binaryType){
// this.ws_protocol = ws_protocol;
// this.ip= ip;
// this.port = port;
// this.heartbeatTimeout = heartbeatTimeout;
// this.reconnectInterval = reconnectInterval;
// this.binaryType = binaryType;
// this.url = ws_protocol + '://' + ip + ':'+ port;
// this.initHandlerList();
// }
connect(isReconncect){
this.ws = new WebSocket(this.url);
console.log("current url "+this.url+" status "+this.ws.readyState);
this.ws.binaryType = this.binaryType;
var websocketObj = this;
this.ws.onopen = function (event) {
console.log("ws open");
websocketObj.isconnected = true;
websocketObj.lastInteractionTime(new Date().getTime());
websocketObj.pingIntervalId = setInterval(() => {
websocketObj.ping();
}, websocketObj.heartbeatTimeout);
websocketObj.userDisconnect = false;
//发送connect指令
websocketObj.sendConnectMessage();
}
this.ws.onmessage = function(event) {
console.log("ws onmessage["+event.data+"]");
websocketObj.processMessage(event.data);
websocketObj.lastInteractionTime(new Date().getTime());
}
this.ws.onclose = function(event) {
websocketObj.isconnected = false;
console.log("ws onclose");
websocketObj.ws.close();
clearInterval(websocketObj.pingIntervalId);
if(!websocketObj.userDisconnect){
console.log("reconnect websocket");
websocketObj.reconnect(event);
}
}
this.ws.onerror = function(event) {
console.log("connect error");
}
}
reconnect(event){
var websocketObj = this;
setTimeout(() => {
websocketObj.connect(true);
}, this.reconnectInterval);
}
lastInteractionTime(actionTime){
this.actionTime = actionTime;
}
getLastActionTime(){
return this.actionTime;
}
ping(){
this.send('心跳内容')
}
send(data){
console.log("send message "+data);
if(this.isconnected){
this.ws.send(data);
} else {
console.log("curent websocket is close");
}
}
/**
* 分发vuex action
*/
sendAction(type,data){
vuexStore.dispatch(type,data);
}
initHandlerList(){
this.handlerList.push(new ConnectAckHandler(this));
this.handlerList.push(new GetFriendResultHandler(this));
this.handlerList.push(new GetUserInfoHandler(this));
this.handlerList.push(new ReceiveMessageHandler(this));
this.handlerList.push(new NotifyMessageHandler(this));
this.handlerList.push(new GetGroupInfoHandler(this));
this.handlerList.push(new SendMessageHandler(this));
this.handlerList.push(new UploadTokenHandler(this));
this.handlerList.push(new SearchUserResultHandler(this));
this.handlerList.push(new FriendAddRequestHandler(this));
this.handlerList.push(new NotifyFriendRequestHandler(this));
this.handlerList.push(new FriendRequestHandler(this));
this.handlerList.push(new HandleFriendRequestHandler(this));
this.handlerList.push(new NotifyFriendHandler(this));
this.handlerList.push(new ModifyInfoHandler(this));
this.handlerList.push(new GetGroupMemberHandler(this));
this.handlerList.push(new CreateGroupHandler(this));
this.handlerList.push(new QuitGroupHandler(this));
this.handlerList.push(new RecallMessageHandler(this));
this.handlerList.push(new NotifyRecallMessageHandler(this));
this.handlerList.push(new AddGroupMemberHandler(this));
this.handlerList.push(new KickGroupMemberHandler(this));
this.handlerList.push(new DismissGroupHandler(this));
this.handlerList.push(new GetMinioUploadUrlHandler(this))
this.handlerList.push(new SetFriendAliasRequestHandler(this))
this.handlerList.push(new LoadRemoteMessageHandler(this))
}
processMessage(data){
var protoObj = JSON.parse(data);
for(var i = 0; i < this.handlerList.length; i++){
if(this.handlerList[i].match(protoObj)){
this.handlerList[i].processMessage(protoObj);
}
}
}
/**
* 链接建立信息
*/
sendConnectMessage(){
console.log("userToken "+localStorage.getItem('vue-token'));
let allToken = decrypt(localStorage.getItem('vue-token'));
console.log("decryptToken "+allToken);
let pwd = allToken.substring(0,allToken.indexOf('|'));
allToken = allToken.substring(allToken.indexOf('|')+1);
let secret = allToken.substring(0,allToken.indexOf('|'));
console.log('[pwd]->'+pwd+' [secret]->'+secret);
let pwdAesBase64 = encrypt(pwd,secret);
console.log('encrypt pwd: '+pwdAesBase64);
var websocketprotomessage = new WebSocketProtoMessage();
websocketprotomessage.setSignal(CONNECT);
var connectMessage = {
userName: LocalStore.getUserId(),
password: pwdAesBase64,
clientIdentifier: localStorage.getItem(KEY_VUE_DEVICE_ID)
}
websocketprotomessage.content = connectMessage;
console.log(websocketprotomessage.toJson());
this.send(websocketprotomessage.toJson());
}
sendDisConnectMessage(){
var websocketprotomessage = new WebSocketProtoMessage();
websocketprotomessage.setSignal(DISCONNECT);
var disconnectMessage = {
clearSession : 1
}
websocketprotomessage.content = disconnectMessage;
console.log(websocketprotomessage.toJson());
this.send(websocketprotomessage.toJson());
this.userDisconnect = true;
}
/**
* 获取朋友列表
*/
getFriend(version = 0){
this.sendPublishMessage(FP,{version : version});
}
searchUser(keyword){
var content = {
keyword: keyword,
fuzzy: 1,
page: 0
}
this.sendPublishMessage(US,content);
}
sendFriendAddRequest(value){
this.sendPublishMessage(FAR,value);
}
getFriendRequest(version){
this.sendPublishMessage(FRP,{
version: version
});
}
handleFriendRequest(value){
this.sendPublishMessage(FHR,value);
}
/**
* 获取用户详细信息
*/
getUserInfos(userIds){
this.sendPublishMessage(UPUI,userIds);
}
async getUserInfo(userId){
var promise = await this.sendPublishMessage(UPUI,[userId]);
return promise
}
/**
*
* @param {用户信息} info: {
* type: 0
* value:
* }
*/
modifyMyInfo(info){
this.sendPublishMessage(MMI,info);
}
/**
*
* @param {群组id} groupId
* @param {是否需要刷新} refresh
*/
getGroupInfo(groupId,refresh){
var groupIds = [];
groupIds.push(groupId);
this.sendPublishMessage(GPGI,groupIds);
}
async getGroupMember(groupId,refresh){
return await this.sendPublishMessage(GPGM,{
groupId: groupId,
version: 0
})
}
async addMembers(groupId,memberIds){
var groupMembers = [];
for(var memberId of memberIds){
var groupMember = new GroupMember();
groupMember.memberId = memberId;
groupMember.type = GroupMemberType.Normal;
groupMembers.push(groupMember);
}
return await this.sendPublishMessage(GAM,{
groupId: groupId,
groupMembers: groupMembers
});
}
async kickeMembers(groupId,memberIds){
return await this.sendPublishMessage(GKM,{
groupId: groupId,
memberIds: memberIds
});
}
async createGroup(groupName,memberIds){
var groupInfo = new GroupInfo();
groupInfo.name = groupName;
groupInfo.type = GroupType.Normal;
var groupMembers = [];
for(var memberId of memberIds){
var groupMember = new GroupMember();
groupMember.memberId = memberId;
groupMember.type = memberId == LocalStore.getUserId() ? GroupMemberType.Owner : GroupMemberType.Normal;
groupMembers.push(groupMember);
}
return await this.sendPublishMessage(GC,{
groupInfo: groupInfo,
groupMembers: groupMembers
});
}
async modifyGroupInfo(info){
return await this.sendPublishMessage(GMI,info);
}
async quitGroup(groupId){
return await this.sendPublishMessage(GQ,{
groupId: groupId
});
}
async dismissGroup(groupId){
return await this.sendPublishMessage(GD,{
groupId: groupId
});
}
async recallMessage(messageUid){
return await this.sendPublishMessage(MR,{
messageUid: messageUid
})
}
async getRemoteMessages(conversation,beforeUid,count){
return await this.sendPublishMessage(LRM,{
beforeUid: beforeUid,
count: count,
conversation: conversation
})
}
pullMessage(messageId,type = 0,pullType = 0,sendMessageCount = 0){
this.sendPublishMessage(MP,{
messageId: messageId,
type: type,
pullType : pullType,
sendMessageCount: sendMessageCount
});
}
getUploadToken(mediaType){
var content = {
mediaType: mediaType
};
this.sendPublishMessage(GQNUT,content);
}
async getMinioUploadUrl(mediaType,key){
var content = {
mediaType: mediaType,
key: key
}
return await this.sendPublishMessage(GMURL,content)
}
async modifyFriendAlias(targetUid,alias){
var content = {
reason: alias,
targetUserId: targetUid
}
return await this.sendPublishMessage(FALS,content)
}
/**
*
* @param {子信令} subsignal
* @param {消息体内容} content
*/
sendPublishMessage(subsignal,content,protoMessageId = 0){
var websocketprotomessage = new WebSocketProtoMessage();
websocketprotomessage.setSignal(PUBLISH);
websocketprotomessage.setSubSignal(subsignal);
websocketprotomessage.setContent(content);
var messageId = LocalStore.getMessageId();
if(messageId > 65535){
messageId = 0;
}
websocketprotomessage.setMessageId(++messageId);
LocalStore.saveMessageId(messageId);
this.send(websocketprotomessage.toJson());
var vueWebSocket = this;
var pubAckPromise = new Promise((resolve) => {
var timeoutId = setTimeout(() => {
if(subsignal == MS){
var failProtoMessage = new WebSocketProtoMessage();
failProtoMessage.setSignal(PUB_ACK);
failProtoMessage.setSubSignal(subsignal)
failProtoMessage.setMessageId(messageId);
failProtoMessage.setContent('')
vueWebSocket.processMessage(failProtoMessage.toJson())
} else {
resolve(new FutureResult(ERROR_CODE,""));
}
},10000);
var resolvePromise = new PromiseResolve(resolve,timeoutId);
resolvePromise.protoMessageId = protoMessageId;
vueWebSocket.resolvePromiseMap.set(messageId,resolvePromise);
});
return pubAckPromise;
}
sendMessage(protoMessage){
this.sendPublishMessage(MS,protoMessage,protoMessage.messageId);
}
}
================================================
FILE: src/websocket/listener/onReceiverMessageListener.js
================================================
export default class OnReceiverMessageListener {
onReceiveMessage(protoMessage){
}
}
================================================
FILE: src/websocket/message/fileMessageContent.js
================================================
import MediaMessageContent from './mediaMessageContent';
import MessageContentMediaType from './messageContentMediaType';
import MessageContentType from './messageContentType';
export default class FileMessageContent extends MediaMessageContent {
name = '';
size = 0;
static FILE_NAME_PREFIX = '[文件] ';
constructor(fileOrLocalPath, remotePath) {
super(MessageContentType.File, MessageContentMediaType.File, fileOrLocalPath, remotePath);
if (typeof File !== 'undefined' && fileOrLocalPath instanceof File) {
this.name = fileOrLocalPath.name;
this.size = fileOrLocalPath.size;
}
}
digest() {
return '[文件]';
}
encode() {
let payload = super.encode();
payload.searchableContent = FileMessageContent.FILE_NAME_PREFIX + this.name;
payload.content = this.size + '';
return payload;
};
decode(payload) {
super.decode(payload);
if(payload.searchableContent){
if(payload.searchableContent.indexOf(FileMessageContent.FILE_NAME_PREFIX) === 0){
this.name = payload.searchableContent.substring(payload.searchableContent.indexOf(FileMessageContent.FILE_NAME_PREFIX) + FileMessageContent.FILE_NAME_PREFIX.length);
}else {
this.name = payload.searchableContent;
}
this.size = this.formateSize(payload.content);
}
}
formateSize(value) { if (null == value || value == '') { return "0 Bytes"; }
var unitArr = new Array("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"); var index = 0;
var srcsize = parseFloat(value); index = Math.floor(Math.log(srcsize) / Math.log(1024));
var size = srcsize / Math.pow(1024, index); size = size.toFixed(2); //保留的小数位数
return size + unitArr[index];
}
}
================================================
FILE: src/websocket/message/imageMessageContent.js
================================================
import MediaMessageContent from './mediaMessageContent';
import MessageContentMediaType from './messageContentMediaType';
import MessageContentType from './messageContentType';
export default class ImageMessageContent extends MediaMessageContent {
// base64 encoded, 不包含头部:data:image/png;base64,
thumbnail;
constructor(fileOrLocalPath, remotePath, thumbnail) {
super(MessageContentType.Image, MessageContentMediaType.Image, fileOrLocalPath, remotePath);
this.thumbnail = thumbnail;
}
digest() {
return '[图片]';
}
encode() {
let payload = super.encode();
payload.mediaType = MessageContentMediaType.Image;
payload.binaryContent = this.thumbnail;
return payload;
};
decode(payload) {
super.decode(payload);
this.thumbnail = payload.binaryContent;
}
}
================================================
FILE: src/websocket/message/mediaMessageContent.js
================================================
import MessageContent from './messageContent'
export default class MediaMessageContent extends MessageContent {
file;
remotePath = '';
localPath = '';
mediaType = 0;
constructor(messageType, mediaType = 0, fileOrLocalPath, remotePath) {
super(messageType);
this.mediaType = mediaType;
this.remotePath = remotePath;
if(typeof fileOrLocalPath === "string"){
this.localPath = fileOrLocalPath;
}else {
this.file = fileOrLocalPath;
if (fileOrLocalPath && fileOrLocalPath.path !== undefined) {
this.localPath = fileOrLocalPath.path;
// attention: 粘贴的时候,path是空字符串,故采用了这个trick
if (this.localPath.indexOf(fileOrLocalPath.name) < 0) {
this.localPath += fileOrLocalPath.name;
}
}
}
}
encode() {
let payload = super.encode();
payload.localMediaPath = this.localPath;
payload.remoteMediaUrl = this.remotePath;
payload.mediaType = this.mediaType;
payload.searchableContent = this.digest();
return payload;
};
decode(payload) {
super.decode(payload);
this.localPath = payload.localMediaPath;
this.remotePath = payload.remoteMediaUrl;
this.mediaType = payload.mediaType;
}
}
================================================
FILE: src/websocket/message/message.js
================================================
import Conversation from '../model/conversation'
import MessageStatus from './messageStatus'
import store from '../../store'
import LocalStore from '../store/localstore';
/***
* message in json format
{
"conversation":{
"conversationType": 0,
"target": "UZUWUWuu",
"line": 0,
}
"from": "UZUWUWuu",
"content": {
"type": 1,
"searchableContent": "1234",
"pushContent": "",
"content": "",
"binaryContent": "",
"localContent": "",
"mediaType": 0,
"remoteMediaUrl": "",
"localMediaPath": "",
"mentionedType": 0,
"mentionedTargets": [ ]
},
"messageId": 52,
"direction": 1,
"status": 5,
"messageUid": 75735276990792720,
"timestamp": 1550849394256,
"to": ""
}
*/
export default class Message {
conversation = {};
from = '';
content = {};
messageId = 0;
direction = 0;
status = 0;
messageUid = 0;
timestamp = 0;
tos = '';
// static toMessage(obj){
// let msg = new Message();
// msg.conversation = new Conversation(obj.conversationType,obj.target,obj.line);
// msg.from = obj.from;
// msg.content = obj.content;
// msg.messageId = obj.messageId;
// if(obj.from == USER_ID){
// msg.direction = 0;
// } else {
// msg.direction = 1;
// }
// msg.status = obj.status;
// msg.messageUid = obj.messageId;
// msg.timestamp = obj.timestamp;
// msg.to = obj.target;
// return msg;
// }
static toMessage(state,sendMessage){
var message = new Message();
var target = sendMessage.target;
if(!target){
target = state.selectTarget;
}
if(sendMessage.tos != ''){
message.tos = sendMessage.tos;
}
console.log("to message target "+target);
let stateConversationInfo = state.conversations.find(conversation => conversation.conversationInfo.target === target);
console.log("conversationtype "+stateConversationInfo.conversationInfo.conversationType +" target "+stateConversationInfo.conversationInfo.target);
message.conversation = new Conversation(stateConversationInfo.conversationInfo.conversationType,
stateConversationInfo.conversationInfo.target,
stateConversationInfo.conversationInfo.line);
console.log("send message content "+sendMessage.messageContent)
message.content = sendMessage.messageContent;
message.from = state.userId;
message.status = MessageStatus.Sending;
message.timestamp = new Date().getTime();
message.direction = 0;
message.messageId = new Date().getTime();
return message;
}
//方法不支持重载
static conert2Message(sendMessage){
var message = new Message();
var target = sendMessage.target;
if(!target){
target = store.state.selectTarget;
}
console.log("to message target "+target);
let stateConversationInfo = store.state.conversations.find(conversation => conversation.conversationInfo.target === target);
console.log("conversationtype "+stateConversationInfo.conversationInfo.conversationType +" target "+stateConversationInfo.conversationInfo.target);
message.conversation = new Conversation(stateConversationInfo.conversationInfo.conversationType,
stateConversationInfo.conversationInfo.target,
stateConversationInfo.conversationInfo.line);
message.content = sendMessage.messageContent;
message.from = LocalStore.getUserId();
message.status = MessageStatus.Sending;
message.timestamp = new Date().getTime();
message.direction = 0;
message.messageId = new Date().getTime();
return message;
}
}
================================================
FILE: src/websocket/message/messageConfig.js
================================================
import PersistFlag from './persistFlag'
import UnknownMessageContent from './unknownMessageContent'
import MessageContentType from './messageContentType'
import TextMessageContent from './textMessageContent'
import UnsupportMessageContent from './unsupportMessageContent'
import CallStartMessageContent from '../../webrtc/message/callStartMessageContent'
import CallAnswerMessageContent from '../../webrtc/message/callAnswerMessageContent'
import CallAnswerTMessageContent from '../../webrtc/message/callAnswerTMessageContent'
import CallSignalMessageContent from '../../webrtc/message/callSignalMessageContent'
import ImageMessageContent from './imageMessageContent'
import CallByeMessageContent from '../../webrtc/message/callByeMessageContent'
import CallModifyMessageContent from '../../webrtc/message/callModifyMessageContent'
import CreateGroupNotification from './notification/createGroupNotification'
import ChangeGroupNameNotification from './notification/changeGroupNameNotification'
import NotificationMessageContent from './notification/notificationMessageContent'
import AddGroupMemberNotification from './notification/addGroupMemberNotification'
import QuitGroupNotification from './notification/quitGroupNotification'
import RecallMessageNotification from './notification/recallMessageNotification'
import KickoffGroupMemberNotification from './notification/kickoffGroupMemberNotification'
import DismissGroupNotification from './notification/dismissGroupNotification'
import LocalStore from '../store/localstore'
import VideoMessageContent from './videoMessageContent'
import FileMessageContent from './fileMessageContent'
export default class MessageConfig{
static getMessageContentClazz(type) {
for (const content of MessageConfig.MessageContents) {
if (content.type === type) {
if (content.contentClazz) {
return content.contentClazz;
} else {
return UnsupportMessageContent;
}
}
}
console.log(`message type ${type} is unknown`);
return UnknownMessageContent;
}
static convert2MessageContent(from,protoMessageContent){
var messageContent = null;
var contentClazz = this.getMessageContentClazz(protoMessageContent.type);
if(contentClazz != UnsupportMessageContent){
let content = new contentClazz();
try {
content.decode(protoMessageContent);
if (content instanceof NotificationMessageContent) {
content.fromSelf = from === LocalStore.getUserId();
}
messageContent = content;
}catch(error){
console.log("decode message content error "+protoMessageContent);
}
}
return messageContent;
}
static getMessageContentPersitFlag(type) {
for (const content of MessageConfig.MessageContents) {
if (content.type === type) {
return content.flag;
}
}
return 0;
}
static isDisplayableMessage(protomessage){
var messageContent = protomessage.content;
if(MessageConfig.getMessageContentPersitFlag(messageContent.type) == PersistFlag.Persist ||
MessageConfig.getMessageContentPersitFlag(messageContent.type) == PersistFlag.Persist_And_Count){
return true;
}
return false;
}
static MessageContents = [
{
name: 'unknown',
flag: PersistFlag.Persist,
type: MessageContentType.Unknown,
contentClazz: UnknownMessageContent,
},
{
name: 'text',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.Text,
contentClazz: TextMessageContent,
},
{
name: 'ptext',
flag: PersistFlag.Persist,
type: MessageContentType.P_Text,
},
{
name: 'voice',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.Voice,
},
{
name: 'image',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.Image,
contentClazz: ImageMessageContent,
},
{
name: 'location',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.Location,
},
{
name: 'file',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.File,
contentClazz: FileMessageContent,
},
{
name: 'video',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.Video,
contentClazz: VideoMessageContent
},
{
name: 'sticker',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.Sticker,
},
{
name: 'imageText',
flag: PersistFlag.Persist_And_Count,
type: MessageContentType.ImageText,
},
{
name: 'tip',
flag: PersistFlag.Persist,
type: MessageContentType.Tip_Notification,
},
{
name: 'typing',
flag: PersistFlag.Transparent,
type: MessageContentType.Typing,
},
{
name: 'addGroupMemberNotification',
flag: PersistFlag.Persist,
type: MessageContentType.AddGroupMember_Notification,
contentClazz: AddGroupMemberNotification
},
{
name: 'changeGroupNameNotification',
flag: PersistFlag.Persist,
type: MessageContentType.ChangeGroupName_Notification,
contentClazz: ChangeGroupNameNotification
},
{
name: 'changeGroupPortraitNotification',
flag: PersistFlag.Persist,
type: MessageContentType.ChangeGroupPortrait_Notification,
},
{
name: 'createGroupNotification',
flag: PersistFlag.Persist,
type: MessageContentType.CreateGroup_Notification,
contentClazz: CreateGroupNotification
},
{
name: 'dismissGroupNotification',
flag: PersistFlag.Persist,
type: MessageContentType.DismissGroup_Notification,
contentClazz: DismissGroupNotification
},
{
name: 'kickoffGroupMemberNotification',
flag: PersistFlag.Persist,
type: MessageContentType.KickOffGroupMember_Notification,
contentClazz: KickoffGroupMemberNotification
},
{
name: 'modifyGroupAliasNotification',
flag: PersistFlag.Persist,
type: MessageContentType.ModifyGroupAlias_Notification,
},
{
name: 'quitGroupNotification',
flag: PersistFlag.Persist,
type: MessageContentType.QuitGroup_Notification,
contentClazz: QuitGroupNotification,
},
{
name: 'transferGroupOwnerNotification',
flag: PersistFlag.Persist,
type: MessageContentType.TransferGroupOwner_Notification,
},
{
name: 'recall',
flag: PersistFlag.Persist,
type: MessageContentType.RecallMessage_Notification,
contentClazz: RecallMessageNotification
},
{
name: 'callStartMessageContent',
flag: PersistFlag.Persist,
type: MessageContentType.VOIP_CONTENT_TYPE_START,
contentClazz: CallStartMessageContent,
},
{
name: 'callAnswerMessageContent',
flag: PersistFlag.No_Persist,
type: MessageContentType.VOIP_CONTENT_TYPE_ACCEPT,
contentClazz: CallAnswerMessageContent,
},
{
name: 'callAnswerTMessageContent',
flag: PersistFlag.Transparent,
type: MessageContentType.VOIP_CONTENT_TYPE_ACCEPT_T,
contentClazz: CallAnswerTMessageContent,
},
{
name: 'callByeMessageContent',
flag: PersistFlag.No_Persist,
type: MessageContentType.VOIP_CONTENT_TYPE_END,
contentClazz: CallByeMessageContent,
},
{
name: 'callSignalMessageContent',
flag: PersistFlag.Transparent,
type: MessageContentType.VOIP_CONTENT_TYPE_SIGNAL,
contentClazz: CallSignalMessageContent
},
{
name: 'callModifyMessageContent',
flag: PersistFlag.No_Persist,
type: MessageContentType.VOIP_CONTENT_TYPE_MODIFY,
contentClazz: CallModifyMessageContent,
},
{
name: 'callAddParticipant',
flag: PersistFlag.Persist,
type: MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT,
},
{
name: 'callMuteVideo',
flag: PersistFlag.No_Persist,
type: MessageContentType.VOIP_CONTENT_TYPE_MUTE_VIDEO,
},
];
}
================================================
FILE: src/websocket/message/messageContent.js
================================================
import MessagePayload from "./messagePayload";
/**
* 消息类型基类,一般包括文本消息,文件类消息,媒体类消息
*/
export default class MessageContent {
type;
//0 普通消息, 1 部分提醒, 2 提醒全部
mentionedType = 0;
//提醒对象,mentionedType 1时有效
mentionedTargets = [];
extra;
constructor(type, mentionedType = 0, mentionedTargets = []) {
this.type = type;
this.mentionedType = mentionedType;
this.mentionedTargets = mentionedTargets;
}
digest() {
return '...digest...';
}
/**
* return MessagePayload in json format
*/
encode() {
let payload = new MessagePayload();
payload.type = this.type;
payload.mentionedType = this.mentionedType;
payload.mentionedTargets = this.mentionedTargets;
return payload;
}
/**
*
* @param {object} payload object json.parse from message#content
*/
decode(payload) {
this.type = payload.type;
this.mentionedType = payload.mentionedType;
this.mentionedTargets = payload.mentionedTargets;
}
}
================================================
FILE: src/websocket/message/messageContentMediaType.js
================================================
export default class MessageContentMediaType {
static General = 0;
static Image = 1;
static Voice = 2;
static Video = 3;
static File = 4;
static Portrait = 5;
static Fvaorite = 6;
}
================================================
FILE: src/websocket/message/messageContentType.js
================================================
export default class MessageContentType {
// 基本消息类型
static Unknown = 0;
static Text = 1;
static Voice = 2;
static Image = 3;
static Location = 4;
static File = 5;
static Video = 6;
static Sticker = 7;
static ImageText = 8;
static P_Text = 9;
// 提醒消息
static RecallMessage_Notification = 80;
static Tip_Notification = 90;
static Typing = 91;
// 群相关消息
static CreateGroup_Notification = 104;
static AddGroupMember_Notification = 105;
static KickOffGroupMember_Notification = 106;
static QuitGroup_Notification = 107;
static DismissGroup_Notification = 108;
static TransferGroupOwner_Notification = 109;
static ChangeGroupName_Notification = 110;
static ModifyGroupAlias_Notification = 111;
static ChangeGroupPortrait_Notification = 112;
static MuteGroupMember_Notification = 113;
static ChangeJoinType_Notification = 114;
static ChangePrivateChat_Notification = 115;
static ChangeSearchable_Notificaiton = 116;
static SetGroupManager_Notification = 117;
static VOIP_CONTENT_TYPE_START = 400;
static VOIP_CONTENT_TYPE_END = 402;
static VOIP_CONTENT_TYPE_ACCEPT = 401;
static VOIP_CONTENT_TYPE_SIGNAL = 403;
static VOIP_CONTENT_TYPE_MODIFY = 404;
static VOIP_CONTENT_TYPE_ACCEPT_T = 405;
}
================================================
FILE: src/websocket/message/messagePayload.js
================================================
/**
*
"content": {
"type": 1,
"searchableContent": "1234",
"pushContent": "",
"content": "",
"binaryContent": "",
"localContent": "",
"mediaType": 0,
"remoteMediaUrl": "",
"localMediaPath": "",
"mentionedType": 0,
"mentionedTargets": [ ]
},
*/
export default class MessagePayload {
type;
searchableContent;
pushContent;
content;
binaryContent; // base64 string, 图片时,不包含头部信息:data:image/png;base64,
localContent;
mediaType;
remoteMediaUrl;
localMediaPath;
mentionedType = 0;
mentionedTargets = [];
extra;
}
================================================
FILE: src/websocket/message/messageStatus.js
================================================
export default class MessageStatus {
static Sending = 0;
static Sent = 1;
static SendFailure = 2;
static Mentioned = 3;
static AllMentioned = 4;
static Unread = 5;
static Readed = 6;
static Played = 7;
}
================================================
FILE: src/websocket/message/modifyGroupInfoType.js
================================================
export default class ModifyGroupInfoType {
static Modify_Group_Name = 0;
static Modify_Group_Portrait = 1;
static Modify_Group_Extra = 2;
}
================================================
FILE: src/websocket/message/myInfoType.js
================================================
export default class MyInfotype {
static Modify_DisplayName = 0;
static Modify_Portrait = 1;
static Modify_Gender = 2;
static Modify_Mobile = 3;
static Modify_Email = 4;
static Modify_Address = 5;
static Modify_Company = 6;
static Modify_Social = 7;
static Modify_Extra = 8;
}
================================================
FILE: src/websocket/message/notification/addGroupMemberNotification.js
================================================
import MessageContentType from '../messageContentType';
import GroupNotificationContent from './groupNotification';
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class AddGroupMemberNotification extends GroupNotificationContent {
invitor = '';
invitees = [];
constructor(invitor, invitees) {
super(MessageContentType.AddGroupMember_Notification);
this.invitor = invitor;
this.invitees = invitees;
}
formatNotification() {
let notifyStr;
if (this.invitees.length === 1 && this.invitees[0] === this.invitor) {
if (this.fromSelf) {
return '您加入了群组';
} else {
return webSocketCli.getDisplayName(this.invitor) + ' 加入了群组';
}
}
if (this.fromSelf) {
notifyStr = '您邀请:';
} else {
notifyStr = webSocketCli.getDisplayName(this.invitor) + '邀请:';
}
let membersStr = '';
this.invitees.forEach(m => {
membersStr += ' ' + webSocketCli.getDisplayName(m);
});
return notifyStr + membersStr + '加入了群组';
}
encode() {
let payload = super.encode();
let obj = {
g: this.groupId,
o: this.invitor,
ms: this.invitees,
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
};
decode(payload) {
super.decode(payload);
let json = StringUtils.b64_to_utf8(payload.binaryContent);
let obj = JSON.parse(json);
this.groupId = obj.g;
this.invitor = obj.o;
this.invitees = obj.ms;
}
}
================================================
FILE: src/websocket/message/notification/changeGroupNameNotification.js
================================================
import MessageContentType from "../messageContentType";
import GroupNotificationContent from "./groupNotification";
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class ChangeGroupNameNotification extends GroupNotificationContent {
operator = '';
name = '';
constructor(operator, name) {
super(MessageContentType.ChangeGroupName_Notification);
this.operator = operator;
this.name = name;
}
formatNotification() {
if (this.fromSelf) {
return '您修改群名称为:' + this.name;
} else {
return webSocketCli.getDisplayName(this.operator)+' 修改群名称为:' + this.name;
}
}
encode() {
let payload = super.encode();
let obj = {
g: this.groupId,
n: this.name,
o: this.operator,
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
}
decode(payload) {
super.decode(payload);
let json = StringUtils.b64_to_utf8(payload.binaryContent)
let obj = JSON.parse(json);
this.groupId = obj.g;
this.operator = obj.o;
this.name = obj.n;
}
}
================================================
FILE: src/websocket/message/notification/createGroupNotification.js
================================================
import MessageContentType from '../messageContentType';
import GroupNotificationContent from './groupNotification';
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class CreateGroupNotification extends GroupNotificationContent {
creator = '';
groupName = '';
constructor(creator, groupName) {
super(MessageContentType.CreateGroup_Notification);
this.creator = creator;
this.groupName = groupName;
}
formatNotification() {
if (this.fromSelf) {
return '您创建了群组 ' + this.groupName;
} else {
return webSocketCli.getDisplayName(this.creator)+' 创建了群组 ' + this.groupName;
}
}
encode() {
let payload = super.encode();
let obj = {
g: this.groupId,
n: this.groupName,
o: this.creator,
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
}
decode(payload) {
super.decode(payload);
let json = StringUtils.b64_to_utf8(payload.binaryContent)
let obj = JSON.parse(json);
this.groupId = obj.g;
this.creator = obj.o;
this.groupName = obj.n;
}
}
================================================
FILE: src/websocket/message/notification/dismissGroupNotification.js
================================================
import MessageContentType from '../messageContentType';
import GroupNotificationContent from './groupNotification';
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class DismissGroupNotification extends GroupNotificationContent {
operator = '';
constructor(operator) {
super(MessageContentType.DismissGroup_Notification);
this.operator = operator;
}
formatNotification() {
if (this.fromSelf) {
return '您解散了群组';
} else {
return webSocketCli.getDisplayName(this.operator) + '解散了群组';
}
}
encode() {
let payload = super.encode();
let obj = {
g: this.groupId,
o: this.operator,
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
}
decode(payload) {
super.decode(payload);
let json = StringUtils.b64_to_utf8(payload.binaryContent)
let obj = JSON.parse(json);
this.groupId = obj.g;
this.operator = obj.o;
}
}
================================================
FILE: src/websocket/message/notification/groupNotification.js
================================================
import NotificationMessageContent from "./notificationMessageContent";
export default class GroupNotificationContent extends NotificationMessageContent {
groupId = '';
}
================================================
FILE: src/websocket/message/notification/kickoffGroupMemberNotification.js
================================================
import MessageContentType from "../messageContentType";
import GroupNotificationContent from "./groupNotification";
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class KickoffGroupMemberNotification extends GroupNotificationContent {
operator = '';
kickedMembers = [];
constructor(operator, kickedMembers) {
super(MessageContentType.KickOffGroupMember_Notification);
this.operator = operator;
this.kickedMembers = kickedMembers;
}
formatNotification() {
let notifyStr;
if (this.fromSelf) {
notifyStr = '您把 ';
} else {
notifyStr = webSocketCli.getDisplayName(this.operator) + '把 ';
}
let kickedMembersStr = '';
this.kickedMembers.forEach(m => {
kickedMembersStr += ' ' + webSocketCli.getDisplayName(m);
});
return notifyStr + kickedMembersStr + ' 移除了群组';
}
encode() {
let payload = super.encode();
let obj = {
g: this.groupId,
ms: this.kickedMembers,
o: this.operateUser,
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
}
decode(payload) {
super.decode(payload);
let json = StringUtils.b64_to_utf8(payload.binaryContent)
let obj = JSON.parse(json);
this.groupId = obj.g;
this.operator = obj.o;
this.kickedMembers = obj.ms;
}
}
================================================
FILE: src/websocket/message/notification/notificationMessageContent.js
================================================
import MessageContent from "../messageContent";
export default class NotificationMessageContent extends MessageContent {
// message#protoMessageToMessage时设置
fromSelf = false;
constructor(type) {
super(type);
}
digest(message) {
var desc = '';
try {
desc = this.formatNotification(message);
} catch (error) {
console.log('disgest', error);
}
return desc;
};
formatNotification(message) {
return '..nofication..';
}
}
================================================
FILE: src/websocket/message/notification/quitGroupNotification.js
================================================
import MessageContentType from '../messageContentType';
import GroupNotificationContent from './groupNotification';
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class QuitGroupNotification extends GroupNotificationContent {
operator = '';
constructor(operator) {
super(MessageContentType.QuitGroup_Notification);
this.operator = operator;
}
formatNotification() {
if (this.fromSelf) {
return '您退出了群组';
} else {
return webSocketCli.getDisplayName(this.operator) + '退出了群组';
}
}
encode() {
let payload = super.encode();
let obj = {
g: this.groupId,
o: this.operator,
};
payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));
return payload;
}
decode(payload) {
super.decode(payload);
let json = StringUtils.b64_to_utf8(payload.binaryContent)
let obj = JSON.parse(json);
this.groupId = obj.g;
this.operator = obj.o;
}
}
================================================
FILE: src/websocket/message/notification/recallMessageNotification.js
================================================
import NotificationMessageContent from './notificationMessageContent'
import MessageContentType from '../messageContentType';
import StringUtils from '../../utils/StringUtil'
import webSocketCli from '../../websocketcli'
export default class RecallMessageNotification extends NotificationMessageContent {
operatorId = '';
messageUid = '';
constructor(operatorId, messageUid) {
super(MessageContentType.RecallMessage_Notification);
this.operatorId = operatorId;
this.messageUid = messageUid;
}
formatNotification() {
if(this.fromSelf){
return "您撤回了一条消息";
}else {
return webSocketCli.getDisplayName(this.operatorId) + "撤回了一条消息";
}
}
encode() {
let payload = super.encode();
payload.content = this.operatorId;
payload.binaryContent = StringUtils.utf8_to_b64(this.messageUid.toString());
return payload;
};
decode(payload) {
super.decode(payload);
this.operatorId = payload.content;
this.messageUid = StringUtils.b64_to_utf8(payload.binaryContent);
}
}
================================================
FILE: src/websocket/message/persistFlag.js
================================================
export default class PersistFlag {
static No_Persist = 0;
static Persist = 1;
static Persist_And_Count = 3;
static Transparent = 4;
}
================================================
FILE: src/websocket/message/protomessage.js
================================================
import MessageStatus from "./messageStatus";
import ConversationType from "../model/conversationType";
import ProtoMessageContent from "./protomessageContent";
import LocalStore from "../store/localstore";
/**
* 聊天信息content,在发送的时候转换为json消息体发送
*/
export default class ProtoMessage {
conversationType;
target;
line;
from = '';
content = {};
messageId = '0';
direction = 0;
status = 0;
messageUid = '0';
timestamp = 0;
tos = '';
static toProtoMessage(obj){
let currentUserId = LocalStore.getUserId();
let protoMessage = new ProtoMessage();
if(obj.from == currentUserId){
protoMessage.direction = 0;
protoMessage.status = MessageStatus.Sent;
} else {
protoMessage.direction = 1;
protoMessage.status = MessageStatus.Unread;
}
protoMessage.tos = obj.tos;
protoMessage.messageId = obj.messageId;
protoMessage.messageUid = obj.messageId;
protoMessage.timestamp = obj.timestamp;
protoMessage.conversationType = obj.conversationType;
protoMessage.target = obj.target;
protoMessage.from = obj.from;
if(protoMessage.conversationType == ConversationType.Single){
if(obj.from != currentUserId){
protoMessage.target = obj.from;
protoMessage.from = obj.from;
} else {
protoMessage.target = obj.target;
protoMessage.from = obj.from;
}
}
protoMessage.line = obj.line;
protoMessage.content = ProtoMessageContent.toProtoMessageContent(obj.content);
return protoMessage;
}
/***
* 转为即将发送的protomessage
*/
static convertToProtoMessage(message){
var protoMessage = new ProtoMessage();
protoMessage.conversationType = message.conversation.type;
protoMessage.target = message.conversation.target;
protoMessage.line = message.conversation.line;
protoMessage.from = message.from;
protoMessage.tos = message.tos;
protoMessage.direction = message.direction;
protoMessage.status = message.status;
protoMessage.messageId = message.messageId;
protoMessage.messageUid = message.messageUid;
protoMessage.timestamp = message.timestamp;
console.log("protomessage content "+message.content)
var payload = message.content.encode();
protoMessage.content = ProtoMessageContent.toProtoMessageContent(payload);
return protoMessage;
}
}
================================================
FILE: src/websocket/message/protomessageContent.js
================================================
import MessageContentType from "./messageContentType";
export default class ProtoMessageContent{
type;
searchableContent;
pushContent;
content;
binaryContent; // base64 string, 图片时,不包含头部信息:data:image/png;base64,
localContent;
mediaType;
remoteMediaUrl;
localMediaPath;
mentionedType = 0;
mentionedTargets = [];
static toProtoMessageContent(content){
var protoMessageContent = new ProtoMessageContent();
protoMessageContent.type = content.type;
protoMessageContent.content = content.content;
protoMessageContent.searchableContent = content.searchableContent;
protoMessageContent.pushContent = content.pushContent;
protoMessageContent.binaryContent = content.binaryContent;
protoMessageContent.localContent = content.localContent;
protoMessageContent.mediaType = content.mediaType;
protoMessageContent.remoteMediaUrl = content.remoteMediaUrl;
protoMessageContent.localMediaPath = content.localMediaPath;
protoMessageContent.mentionedType = content.mentionedType;
protoMessageContent.mentionedTargets = content.mentionedTargets;
return protoMessageContent;
}
static typeToContent(messageContent){
var showText = "您有新的消息";
switch(messageContent.type){
case MessageContentType.Image:
showText = "[图片]";
break;
case MessageContentType.File:
showText = "[文件]";
break;
case MessageContentType.Video:
showText = "[视频]";
break;
case MessageContentType.VOIP_CONTENT_TYPE_START:
showText = "[网络电话]";
break;
case MessageContentType.Tip_Notification:
showText = '[通知消息]';
}
return showText;
}
}
================================================
FILE: src/websocket/message/sendMessage.js
================================================
export default class SendMessage{
target;
messageContent;
tos;
constructor(target,messageContent,tos=''){
this.target = target;
this.messageContent = messageContent;
this.tos = tos;
}
}
================================================
FILE: src/websocket/message/textMessageContent.js
================================================
import MessageContent from './messageContent'
import MessagePayload from './messagePayload';
import MessageContentType from './messageContentType';
export default class TextMessageContent extends MessageContent {
content;
constructor(content, mentionedType = 0, mentionedTargets = []) {
super(MessageContentType.Text, mentionedType, mentionedTargets);
this.content = content;
}
digest() {
return this.content;
}
encode() {
let payload = super.encode();
payload.searchableContent = this.content;
return payload;
};
decode(payload) {
super.decode(payload);
this.content = payload.searchableContent;
}
}
================================================
FILE: src/websocket/message/unknownMessageContent.js
================================================
import MessageContent from "./messageContent";
import MessageContentType from "./messageContentType";
export default class UnknownMessageContent extends MessageContent {
originalPayload;
constructor(originalPayload) {
super(MessageContentType.Unknown);
this.originalPayload = originalPayload;
}
encode() {
return this.originalPayload;
}
decode(paylaod) {
this.originalPayload = paylaod;
}
digest() {
return '未知类型消息';
}
}
================================================
FILE: src/websocket/message/unsupportMessageContent.js
================================================
import MessageContent from "./messageContent";
export default class UnsupportMessageContent extends MessageContent {
digest() {
return '尚不支持该类型消息, 请手机查看 : ' + this.type;
}
}
================================================
FILE: src/websocket/message/videoMessageContent.js
================================================
import MediaMessageContent from './mediaMessageContent'
import MessageContentMediaType from './messageContentMediaType';
import MessageContentType from './messageContentType';
export default class VideoMessageContent extends MediaMessageContent {
// base64 encoded
thumbnail;
constructor(fileOrLocalPath, remotePath, thumbnail) {
super(MessageContentType.Video, MessageContentMediaType.Video, fileOrLocalPath, remotePath);
this.thumbnail = thumbnail;
}
digest() {
return '[视频]';
}
encode() {
let payload = super.encode();
payload.binaryContent = this.thumbnail;
payload.mediaType = MessageContentMediaType.Video;
return payload;
};
decode(payload) {
super.decode(payload);
this.thumbnail = payload.binaryContent;
}
}
================================================
FILE: src/websocket/message/websocketprotomessage.js
================================================
/**
* websocket json 主协议
*
* {
"signal": "connect",
"sub_signal": "conect_ack",
"message_id": 0,
"content": ""
}
*/
export class WebSocketProtoMessage {
signal;
subSignal;
constructor(){
console.log('constuctor WebSocketProtoMessage');
}
setMessageId(messageId){
this.messageId = messageId;
}
setSignal(signal){
this.signal = signal;
}
setSubSignal(subSignal){
this.subSignal = subSignal;
}
setContent(content){
this.content = content;
}
toJson(){
let message = {
signal : this.signal,
subSignal : this.subSignal == null ? 'NONE' : this.subSignal,
messageId : this.messageId == null ? 0 : this.messageId,
content : this.content
}
return JSON.stringify(message);
}
}
================================================
FILE: src/websocket/model/conversation.js
================================================
import ConversationType from "./conversationType";
/**
*
"conversation":{
"conversationType": 0,
"target": "UZUWUWuu",
"line": 0,
}
*/
export default class Conversation {
type = ConversationType.Single;
conversationType = this.type; // 这行是为了做一个兼容处理
target = '';
line = 0;
constructor(type, target, line) {
this.type = type;
this.conversationType = type;
this.target = target;
this.line = line;
}
equal(conversation) {
return this.type === conversation.type
&& this.target === conversation.target
&& this.line === conversation.line;
}
}
================================================
FILE: src/websocket/model/conversationInfo.js
================================================
export default class ConversationInfo{
conversation = {};
lastMessage = {};
timestamp = 0;
draft = '';
unreadCount = {};
isTop = false;
isSilent = false;
}
================================================
FILE: src/websocket/model/conversationType.js
================================================
export default class ConversationType {
static Single = 0;
static Group = 1;
static ChatRoom = 2;
static Channel = 3;
}
================================================
FILE: src/websocket/model/groupInfo.js
================================================
import GroupType from './groupType'
export default class GroupInfo {
target = '';
name = '';
portrait = '';
owner = '';
type = GroupType.Normal;
memberCount = 0;
extra = '';
updateDt = 0;
static convert2GroupInfo(jsonObj){
var groupInfo = new GroupInfo();
groupInfo.target = jsonObj.target;
groupInfo.name = jsonObj.name;
groupInfo.portrait = jsonObj.portrait;
groupInfo.owner = jsonObj.owner;
groupInfo.type = jsonObj.type;
groupInfo.memberCount = jsonObj.memberCount;
groupInfo.extra = jsonObj.extra;
groupInfo.updateDt = jsonObj.updateDt;
return groupInfo;
}
}
================================================
FILE: src/websocket/model/groupMember.js
================================================
export default class GroupMember {
groupId = '';
memberId = '';
alias = '';
type = 0;
updateDt = 0;
diplayName = '';
avatarUrl = '';
static convert2GroupMember(jsonObj){
var groupMember = new GroupMember();
groupMember.memberId = jsonObj.memberId;
groupMember.alias = jsonObj.alias;
groupMember.type = jsonObj.type;
groupMember.updateDt = jsonObj.updateDt;
return groupMember;
}
}
================================================
FILE: src/websocket/model/groupMemberType.js
================================================
export default class GroupMemberType {
static Normal = 0;
static Manager = 1;
static Owner = 2;
}
================================================
FILE: src/websocket/model/groupType.js
================================================
export default class GroupType {
static Normal = 0;
static Free = 1;
static Restricted = 2;
}
================================================
FILE: src/websocket/model/protoConversationInfo.js
================================================
import UnreadCount from "./unReadCount";
export default class ProtoConversationInfo{
conversationType;
target;
line;
lastMessage = {};
timestamp = 0;
draft = '';
unreadCount = new UnreadCount();
isTop = false;
isSilent = false;
}
================================================
FILE: src/websocket/model/stateConversationInfo.js
================================================
export default class StateConversationInfo{
name;
img;
//ProtoConversationInfo
conversationInfo = {};
}
================================================
FILE: src/websocket/model/stateSelectChatMessage.js
================================================
export default class StateSelectChateMessage{
name = '';
target;
protoMessages = [];
}
================================================
FILE: src/websocket/model/unReadCount.js
================================================
export default class UnreadCount {
// 单聊未读数
unread = 0;
// 群聊@数
unreadMention = 0;
// 群聊@All数
unreadMentionAll = 0;
}
================================================
FILE: src/websocket/model/userInfo.js
================================================
export default class UserInfo {
uid = '';
name = '';
displayName = '';
gender = 0;
portrait = '';
mobile = '';
email = '';
address = '';
social = '';
extra = '';
type = 0; //0 normal; 1 robot; 2 thing;
updateDt = 1550652404513;
static convert2UserInfo(jsonObj){
let userInfo = new UserInfo();
userInfo.uid = jsonObj.uid;
userInfo.name = jsonObj.name;
userInfo.displayName = jsonObj.displayName;
userInfo.gender = jsonObj.gender;
userInfo.portrait = jsonObj.portrait;
userInfo.mobile = jsonObj.mobile;
userInfo.email = jsonObj.email;
userInfo.address = jsonObj.address;
userInfo.social = jsonObj.social;
userInfo.extra = jsonObj.extra;
userInfo.type = jsonObj.type;
userInfo.updateDt = jsonObj.updateDt;
return userInfo;
}
}
================================================
FILE: src/websocket/store/localstore.js
================================================
import { KEY_VUE_USER_ID } from "../../constant";
/**
* 本地缓存,包括如下基本信息
* 消息缓存
* 消息当前序列号,用于消息拉取
* 朋友列表信息
* 群组信息
*/
export default class LocalStore {
static saveConverSations(value){
localStorage.setItem("coversations",JSON.stringify(value));
}
static getConversations(){
let vaule = localStorage.getItem("coversations");
return JSON.parse(vaule);
}
static getLastMessageSeq(){
return localStorage.getItem("last_message_seq");
}
//设置上传token,根据不同media类型存储
static setUploadToken(key,token){
localStorage.setItem(key,token);
}
static getImageUploadToken(){
return localStorage.getItem("http://image.comsince.cn/");
}
/**
* messageSeq为string类型
*/
static setLastMessageSeq(messageSeq){
localStorage.setItem("last_message_seq",messageSeq);
}
static saveMessages(value){
localStorage.setItem("messages",JSON.stringify(value));
}
static getMessages(){
let value = localStorage.getItem("messages");
return JSON.parse(value);
}
static saveUserInfoList(value){
localStorage.setItem("user_infos_list",JSON.stringify(value));
}
static getUserInfoList(){
let value = localStorage.getItem("user_infos_list");
return JSON.parse(value);
}
/**
* 记录消息发送条数主要时为了
*/
static updateSendMessageCount(){
let value = localStorage.getItem("send_message_count");
if(value){
value = parseInt(value) + 1;
} else {
value = 1;
}
localStorage.setItem("send_message_count",value);
}
static getSendMessageCount(){
let value = localStorage.getItem("send_message_count");
if(!value){
value = 0;
}
return value;
}
static resetSendMessageCount(){
localStorage.setItem("send_message_count",0);
}
static getUserId(){
return localStorage.getItem(KEY_VUE_USER_ID);
}
static setSelectTarget(value){
localStorage.setItem("select_target",value);
}
static getSelectTarget(){
return localStorage.getItem("select_target");
}
static setFriendRequestVersion(version){
localStorage.setItem("friend_request_version",version);
}
static getFriendRequestVersion(){
var version = localStorage.getItem("friend_request_version");
if(!version){
version = 0
}
return version;
}
static saveMessageId(messageId){
localStorage.setItem("message_id",messageId);
}
static getMessageId(){
var messageId = localStorage.getItem("message_id");
if(!messageId){
messageId = 0;
}
return messageId;
}
static clearLocalStore(){
localStorage.setItem("coversations","");
localStorage.setItem("last_message_seq","");
localStorage.setItem("messages","");
localStorage.setItem("send_message_count",0);
localStorage.setItem("select_target","");
localStorage.setItem("friend_request_version",0);
localStorage.setItem(KEY_VUE_USER_ID,'');
localStorage.setItem("message_id",0);
}
}
================================================
FILE: src/websocket/utils/StringUtil.js
================================================
export default class StringUtils {
static b64_to_utf8(str) {
return decodeURIComponent(escape(atob(str)));
}
static utf8_to_b64(str) {
return btoa(unescape(encodeURIComponent(str)));
}
}
================================================
FILE: src/websocket/utils/aes.js
================================================
import CryptoJS from 'crypto-js'
const iv = CryptoJS.enc.Base64.parse('ABEiM0RVZnd4eXp7fH1+fw==');
export function decrypt (text) {
let decrypted = CryptoJS.AES.decrypt(text, iv, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
//由于服务端加密的时候前四个字节时间参数,现在暂时忽略前4字节
var resultWordArr = CryptoJS.enc.Hex.parse(decrypted.toString().slice(8));
let result = resultWordArr.toString(CryptoJS.enc.Utf8);
// let rmtimeResult = result.slice(4);
return result;
}
export function encrypt(encryptCode,key) {
console.log('code '+encryptCode+' key '+key);
let keyArry = new Array(16);
for(let i = 0; i<16; i++){
//这里是至打印字符编码
keyArry[i] = key.charCodeAt(i) & 0xFF;
}
let keyCode = String.fromCharCode.apply(String, keyArry);
let secretCode = CryptoJS.enc.Utf8.parse(keyCode);
console.log('keycode '+keyCode);
let timeEncryptCode = convertTimeEncryptCode(encryptCode);
console.log('timeEncryptCode '+timeEncryptCode);
//pwd 必须base64解码
let encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Base64.parse(timeEncryptCode), secretCode, {
iv: secretCode,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
function convertTimeEncryptCode(encryptCode){
// let encryptArr = string2Bin(encryptCode);
let encryptArr = CryptoJS.enc.Base64.parse(encryptCode);
encryptArr = wordToByteArray(encryptArr.words);
// encryptArr = string2Bin(encryptArr);
let result = [];
let curhour = (new Date().getMilliseconds()/1000 - 1514736000)/3600;
for(var i=0; i < encryptArr.length + 4; i++){
if(i < 4){
if(i == 0){
result.push(curhour & 0xFF);
} else if(i == 1){
result.push((curhour & 0xFF00) >> 8);
} else if(i == 2){
result.push((curhour & 0xFF0000) >> 16);
} else if(i == 3){
result.push((curhour & 0xFF) >> 24);
}
} else{
result.push(encryptArr[i - 4]);
}
}
console.log('convertTimeEncryptCode result '+result);
// https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
var base64wordarr = btoa(String.fromCharCode(...new Uint8Array(result)));
console.log("hash word "+base64wordarr);
return base64wordarr;
}
function bin2String(array) {
var result = "";
for (var i = 0; i < array.length; i++) {
result += String.fromCharCode(parseInt(array[i], 2));
}
return result;
}
function string2Bin(str) {
var result = [];
for (var i = 0; i < str.length; i++) {
result.push(str.charCodeAt(i).toString(2));
}
return result;
}
function wordToByteArray(wordArray) {
var byteArray = [], word, i, j;
for (i = 0; i < wordArray.length; ++i) {
word = wordArray[i];
for (j = 3; j >= 0; --j) {
byteArray.push((word >> 8 * j) & 0xFF);
}
}
return byteArray;
}
function byteArrayToString(byteArray) {
var str = "", i;
for (i = 0; i < byteArray.length; ++i) {
str += escape(String.fromCharCode(byteArray[i]));
}
return str;
}
================================================
FILE: src/websocket/utils/logger.js
================================================
export default class Logger {
static log(text){
var time = new Date();
console.log("[" + time.toLocaleTimeString() + "] " + text);
}
}
================================================
FILE: src/websocket/utils/timeUtils.js
================================================
export default class TimeUtils {
//参考链接: https://juejin.im/entry/5c7103f5f265da2dab17d1a6
static _formatDate(date, fmt){
var o = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
static getTimeStringAutoShort2(timestamp,mustIncludeTime){
// 当前时间
var currentDate = new Date();
// 目标判断时间
var srcDate = new Date(parseInt(timestamp));
var currentYear = currentDate.getFullYear();
var currentMonth = (currentDate.getMonth()+1);
var currentDateD = currentDate.getDate();
var srcYear = srcDate.getFullYear();
var srcMonth = (srcDate.getMonth()+1);
var srcDateD = srcDate.getDate();
var ret = "";
// 要额外显示的时间分钟
var timeExtraStr = (mustIncludeTime?" "+this._formatDate(srcDate, "hh:mm"):"");
// 当年
if(currentYear == srcYear) {
var currentTimestamp = currentDate.getTime();
var srcTimestamp = timestamp;
// 相差时间(单位:毫秒)
var deltaTime = (currentTimestamp-srcTimestamp);
// 当天(月份和日期一致才是)
if(currentMonth == srcMonth && currentDateD == srcDateD) {
// 时间相差60秒以内
if(deltaTime < 60 * 1000)
ret = "刚刚";
// 否则当天其它时间段的,直接显示“时:分”的形式
else
ret = this._formatDate(srcDate, "hh:mm");
}
// 当年 && 当天之外的时间(即昨天及以前的时间)
else {
// 昨天(以“现在”的时候为基准-1天)
var yesterdayDate = new Date();
yesterdayDate.setDate(yesterdayDate.getDate()-1);
// 前天(以“现在”的时候为基准-2天)
var beforeYesterdayDate = new Date();
beforeYesterdayDate.setDate(beforeYesterdayDate.getDate()-2);
// 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值
// 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00,
// 这两者间只相差2小时,直接用“deltaTime/(3600 * 1000)” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了)
if(srcMonth == (yesterdayDate.getMonth()+1) && srcDateD == yesterdayDate.getDate())
ret = "昨天"+timeExtraStr;// -1d
// “前天”判断逻辑同上
else if(srcMonth == (beforeYesterdayDate.getMonth()+1) && srcDateD == beforeYesterdayDate.getDate())
ret = "前天"+timeExtraStr;// -2d
else{
// 跟当前时间相差的小时数
var deltaHour = (deltaTime/(3600 * 1000));
// 如果小于或等 7*24小时就显示星期几
if (deltaHour <= 7*24){
var weekday=new Array(7);
weekday[0]="星期日";
weekday[1]="星期一";
weekday[2]="星期二";
weekday[3]="星期三";
weekday[4]="星期四";
weekday[5]="星期五";
weekday[6]="星期六";
// 取出当前是星期几
var weedayDesc = weekday[srcDate.getDay()];
ret = weedayDesc+timeExtraStr;
}
// 否则直接显示完整日期时间
else
ret = this._formatDate(srcDate, "yy/M/d")+timeExtraStr;
}
}
}
// 往年
else{
ret = this._formatDate(srcDate, "yy/M/d")+timeExtraStr;
}
return ret;
}
}
================================================
FILE: src/websocket/websocketcli.js
================================================
import Logger from "./utils/logger";
import vuexStore from '../store'
export class WebSocketClient {
getDisplayName(userId){
var userInfolist = vuexStore.state.userInfoList;
var userInfo = userInfolist.find(user => user.uid == userId);
var displayName = userId;
var friendData = vuexStore.state.friendDatas.find(friend => friend.friendUid == userId)
if(friendData && friendData.alias && friendData.alias != ""){
displayName = friendData.alias
} else if(userInfo){
displayName = userInfo.displayName;
if(displayName == ''){
displayName = userInfo.mobile;
}
} else {
vuexStore.state.vueSocket.getUserInfo(userId);
}
// console.log("userId "+userId +" displayName "+displayName)
return displayName;
}
getPortrait(userId){
var userInfolist = vuexStore.state.userInfoList;
var userInfo = userInfolist.find(user => user.uid == userId);
var portrait = 'static/images/vue.jpg'
if(userInfo){
portrait = userInfo.portrait;
}
return portrait;
}
createGroup(groupName,memberIds){
return vuexStore.state.vueSocket.createGroup(groupName,memberIds);
}
modifyGroupInfo(info){
return vuexStore.state.vueSocket.modifyGroupInfo(info);
}
quitGroup(groupId){
return vuexStore.state.vueSocket.quitGroup(groupId);
}
dismissGroup(groupId){
return vuexStore.state.vueSocket.dismissGroup(groupId);
}
getGroupMember(groupId){
return vuexStore.state.vueSocket.getGroupMember(groupId);
}
addMembers(groupId, memberIds){
return vuexStore.state.vueSocket.addMembers(groupId,memberIds);
}
kickeMembers(groupId,memberIds){
return vuexStore.state.vueSocket.kickeMembers(groupId,memberIds);
}
recallMessage(messageUid){
return vuexStore.state.vueSocket.recallMessage(messageUid);
}
getMinioUploadUrl(mediaType,key){
return vuexStore.state.vueSocket.getMinioUploadUrl(mediaType,key);
}
modifyFriendAlias(targetUid,alias){
return vuexStore.state.vueSocket.modifyFriendAlias(targetUid,alias)
}
//注意beforeUid最好为string类型
getRemoteMessages(conversation,beforeUid,count){
return vuexStore.state.vueSocket.getRemoteMessages(conversation,beforeUid,count)
}
}
const self = new WebSocketClient();
export default self;
================================================
FILE: static/.gitkeep
================================================
================================================
FILE: static/css/reset.css
================================================
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header,
menu, nav, output, ruby, section, summary,
time, mark, audio, video, input {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: normal;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, menu, nav, section {
display: block;
}
body {
line-height: 1;
overflow: hidden;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a {
color: #7e8c8d;
text-decoration: none;
-webkit-backface-visibility: hidden;
}
li {
list-style: none;
}
html, body {
width: 100%;
height: 100%;
}
body {
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
background: #466368;
background: -webkit-linear-gradient(#648880, #293f50);
background: -moz-linear-gradient(#648880, #293f50);
background: linear-gradient(#648880, #293f50);
background-size: cover;
}
/*在mac上,增加此设置,会导致滚动条不能自动显示与隐藏,滚动条会一直显示 */
::-webkit-scrollbar {
width: 8px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 6px;
background: rgba(0,0,0,0.1);
}
/* video {
background: #222;
margin: 0 0 20px 0;
--width: 100%;
width: var(--width);
height: var(--width);
} */
*,*:before,*:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box
}
.flexbox{display:-webkit-box; display:-webkit-flex; display:flex; display:-ms-flexbox;}
.flex-alignc{align-items: center;}
.flex1{-webkit-box-flex:1; -webkit-flex:1; -ms-flex:1; flex:1;}