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 ================================================ [![star](https://gitee.com/comsince/vue-chat/badge/star.svg?theme=white)](https://gitee.com/comsince/vue-chat) [![GitHub stars](https://img.shields.io/github/stars/comsince/vue-chat?style=social)](https://github.com/comsince/vue-chat) # 飞享 ![image](http://image.comsince.cn/fx-chat.png) **NOTE:** [飞享]IM系统开始进行商业化探索,欢迎有需要的`个人`,`企业`, `工作室`使用,关于授权合作事项,请咨询QQ `1282212195` 该项目是`飞享`聊天系统客户端源码vue即时通讯web端实现,使用websocket进行消息通讯,支持文本,图片类型发送,支持实时音视频,支持音视频与[android-chat](https://github.com/fsharechat/android-chat)客户端互通 # 项目截图 * 消息提示 ![image](./attachment/vue-chat-unread.png) * 文字消息 ![image](./attachment/vue-chat.png) * 图片消息 ![image](./attachment/vue-chat-pic.png) * 视频消息 ![image](./attachment/vue-chat-video.png) # 项目演示 * [项目公测地址](https://chat.comsince.cn) * 请选择其中任何一个帐号密码进行登录即可 ```properties 帐号:13800000000, 13800000001, 13800000002 密码:556677 ``` * 暂时停止手机验证码注册登录,后续开通QQ群里面通知 ## 版本规划 ### V1.0.0 * 登录认证流程 * 实现朋友列表展示,用户信息获取 * 会话信息拉取,会话消息缓存 * 纯文本消息通讯 * 支持图片,视频消息展示 * 群会话功能 ### V1.0.1 * 增加全屏幕模式支持,点击用户头像即可切换 ![image](https://user-gold-cdn.xitu.io/2020/4/13/171719952947e62a?w=1518&h=655&f=png&s=170160) ### V1.0.2 * 计划增加一对一音视频聊天功能 * 实现与[android](https://github.com/fsharechat/android-chat)客户端音视频互通 ### V1.0.3 * 增加好友搜索,好友添加功能,形成功能闭环 ### V1.0.4 * 群组用户列表功能 ### V1.0.5 * 增加websocket异步回调接口 * 增加创建群组功能 * 退出群聊 * 撤回消息 * 群组踢人与拉人 * 修改群名称 ![image](https://user-gold-cdn.xitu.io/2020/5/8/171f4c271ba2b4dd?w=2064&h=1144&f=png&s=428322) ### 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 ================================================ ================================================ FILE: src/components/friendlist/friendlist.vue ================================================ ================================================ FILE: src/components/info/info.vue ================================================ ================================================ FILE: src/components/menu/addtip.vue ================================================ ================================================ FILE: src/components/menu/groupInfo.vue ================================================ ================================================ FILE: src/components/menu/personalCard.vue ================================================ ================================================ FILE: src/components/menu/relayMessage.vue ================================================ ================================================ FILE: src/components/menu/rightMenu.vue ================================================ ================================================ FILE: src/components/message/message.vue ================================================ ================================================ 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 ================================================ ================================================ FILE: src/page/group/creategroup.vue ================================================ ================================================ FILE: src/page/group/groupVideoCall.vue ================================================ ================================================ 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