[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\"env\", { \"modules\": false }],\n    \"stage-2\"\n  ],\n  \"plugins\": [\"transform-runtime\"],\n  \"comments\": false,\n  \"env\": {\n    \"test\": {\n      \"presets\": [\"env\", \"stage-2\"],\n      \"plugins\": [ \"istanbul\" ]\n    }\n  }\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules/\ndist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": ".postcssrc.js",
    "content": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  \"plugins\": {\n    // to edit target browsers: use \"browserlist\" field in package.json\n    \"autoprefixer\": {}\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "Creative Commons Attribution-NonCommercial 3.0 Unported\n\nCREATIVE 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.\n\nLicense\n\nTHE 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.\n\nBY 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.\n\n1. Definitions\na. \"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.\nb. \"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.\nc. \"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.\nd. \"Licensor\" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License.\ne. \"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.\nf. \"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.\ng. \"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.\nh. \"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.\ni. \"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.\n2. 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.\n3. 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:\na. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections;\nb. 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.\";\nc. to Distribute and Publicly Perform the Work including as incorporated in Collections; and,\nd. to Distribute and Publicly Perform Adaptations.\nThe 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).\n\n4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:\na. 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.\nb. 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.\nc. 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.\nd. For the avoidance of doubt:\ni. 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;\nii. 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,\niii. 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).\ne. 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.\n5. Representations, Warranties and Disclaimer\nUNLESS 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.\n\n6. 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.\n7. Termination\na. 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.\nb. 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.\n8. Miscellaneous\na. 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.\nb. 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.\nc. 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.\nd. 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.\ne. 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.\nf. 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.\nCreative Commons Notice\n\nCreative 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.\n\nExcept 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.\n\nCreative Commons may be contacted at http://creativecommons.org/."
  },
  {
    "path": "README.md",
    "content": "\n[![star](https://gitee.com/comsince/vue-chat/badge/star.svg?theme=white)](https://gitee.com/comsince/vue-chat)\n[![GitHub stars](https://img.shields.io/github/stars/comsince/vue-chat?style=social)](https://github.com/comsince/vue-chat)\n\n# 飞享\n\n![image](http://image.comsince.cn/fx-chat.png)\n\n**NOTE:** [飞享]IM系统开始进行商业化探索,欢迎有需要的`个人`,`企业`, `工作室`使用,关于授权合作事项,请咨询QQ `1282212195` \n\n该项目是`飞享`聊天系统客户端源码vue即时通讯web端实现，使用websocket进行消息通讯，支持文本，图片类型发送，支持实时音视频，支持音视频与[android-chat](https://github.com/fsharechat/android-chat)客户端互通\n\n# 项目截图\n\n* 消息提示\n\n![image](./attachment/vue-chat-unread.png)\n\n* 文字消息\n\n![image](./attachment/vue-chat.png)\n\n* 图片消息\n\n![image](./attachment/vue-chat-pic.png)\n\n* 视频消息\n\n![image](./attachment/vue-chat-video.png)\n\n# 项目演示\n\n* [项目公测地址](https://chat.comsince.cn)\n* 请选择其中任何一个帐号密码进行登录即可  \n\n```properties\n帐号：13800000000, 13800000001, 13800000002\n密码：556677\n```\n* 暂时停止手机验证码注册登录，后续开通QQ群里面通知\n\n## 版本规划\n### V1.0.0\n* 登录认证流程\n* 实现朋友列表展示，用户信息获取\n* 会话信息拉取，会话消息缓存\n* 纯文本消息通讯\n* 支持图片，视频消息展示\n* 群会话功能\n\n### V1.0.1\n* 增加全屏幕模式支持，点击用户头像即可切换\n\n![image](https://user-gold-cdn.xitu.io/2020/4/13/171719952947e62a?w=1518&h=655&f=png&s=170160)\n\n### V1.0.2\n* 计划增加一对一音视频聊天功能\n* 实现与[android](https://github.com/fsharechat/android-chat)客户端音视频互通\n\n### V1.0.3\n* 增加好友搜索，好友添加功能，形成功能闭环\n\n### V1.0.4\n* 群组用户列表功能\n\n### V1.0.5\n* 增加websocket异步回调接口\n* 增加创建群组功能\n* 退出群聊\n* 撤回消息\n* 群组踢人与拉人\n* 修改群名称\n\n![image](https://user-gold-cdn.xitu.io/2020/5/8/171f4c271ba2b4dd?w=2064&h=1144&f=png&s=428322)\n\n### V1.0.6\n* 增加解散群组的功能\n* 优化群组退出与解散交互体验\n* 对于解散的群组与退出的群组,做删除会话处理\n\n### V1.0.7\n* 增加删除消息的功能\n* 增加转发消息\n\n### V1.0.8\n* 支持缩略图传输，防止android 客户端转发图片报错\n\n### V1.0.9\n* 支持缩略图显示\n\n### V1.0.14\n* 修复群组管理员撤回其他成员发送消息的问题\n\n### V1.1.0\n* 加入群组音视频功能\n\n### V1.1.3\n* 增加文件发送功能\n* 增加通知短音提示\n* 增加音视频通话铃声提示\n* 增加截图粘贴发送功能\n\n### V1.1.4\n* 限制每条会话的消息条数,发送消息时才会删除过多的消息,接收消息时有可能会删除历史未读消息,所以接收时暂不删除过多的消息\n\n\n## Build Setup\n\n``` bash\n# install dependencies\nnpm install\n\n# serve with hot reload at localhost:9080\nnpm run dev\n# 运行请先检查如下配置：TCP服务配置，HTTPS配置，是否支持WSS,是否支持HTTPS，HTTP监听端口8081,HTTPS监听端口8443\n\n# build for production with minification\nnpm run build\n\n# build for production and view the bundle analyzer report\nnpm run build --report\n```\n\nFor 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).\n\n\n\n## 参考项目\n\n* [Vue-chat](https://github.com/han960619/Vue-chat/)\n\n## 依赖组件\n* [常用的 vue 视频插件](https://wangchaoke.cn/?p=372)\n* [西瓜播放器](http://h5player.bytedance.com/gettingStarted)\n* [图标Icon支持](https://www.iconfont.cn/manage/index?spm=a313x.7781069.1998910419.11&manage_type=myprojects&projectId=1698562)\n\n## 推荐项目\n\n* [vue-wechat](https://github.com/zhaohaodang/vue-WeChat)\n* [vue-chat](https://github.com/aermin/vue-chat)\n* [QRCodeLogin](https://github.com/HeyJC/QRCodeLogin/blob/master/Web/auth/src/components/Input.vue) 说明二维码和密码登录的切换操作\n\n\n## 开源协议\n\n本项目使用非商业性署名协议,禁止演绎[Creative Commons Attribution Non Commercial 3.0 Unported](LICENSE)\n\n## 一次性赞助\n\n但是随着项目的增长，也需要相应的资金支持，你可以通过以下方式来赞助此项目\n\n| 支付宝      | 微信| \n| :--------: | :--------:| \n|<img src=\"http://image.comsince.cn/zfb-purse.png\" alt=\"图片替换文本\" width=\"300\" height=\"300\" align=\"center\" />|<img src=\"http://image.comsince.cn/wx-purse.png\" alt=\"图片替换文本\" width=\"300\" height=\"300\" align=\"center\" />|\n\n## QQ 群交流\n\n| QQ群      | \n| :--------: |\n|<img src=\"http://image.comsince.cn/1-VYVLVL22-1587711095978-/storage/emulated/0/Tencent/QQ_Images/qrcode_1587711062833.jpg\" alt=\"图片替换文本\" width=\"300\" height=\"400\" align=\"center\" />|\n\n## 技术支持\n\n如果公司采用本项目或者需要有商业需求，需要二次开发,提供技术支持,联系QQ：`1282212195`"
  },
  {
    "path": "build/build.js",
    "content": "require('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nvar ora = require('ora')\nvar rm = require('rimraf')\nvar path = require('path')\nvar chalk = require('chalk')\nvar webpack = require('webpack')\nvar config = require('../config')\nvar webpackConfig = require('./webpack.prod.conf')\n\nvar spinner = ora('building for production...')\nspinner.start()\n\nrm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {\n  if (err) throw err\n  webpack(webpackConfig, function (err, stats) {\n    spinner.stop()\n    if (err) throw err\n    process.stdout.write(stats.toString({\n      colors: true,\n      modules: false,\n      children: false,\n      chunks: false,\n      chunkModules: false\n    }) + '\\n\\n')\n\n    console.log(chalk.cyan('  Build complete.\\n'))\n    console.log(chalk.yellow(\n      '  Tip: built files are meant to be served over an HTTP server.\\n' +\n      '  Opening index.html over file:// won\\'t work.\\n'\n    ))\n  })\n})\n"
  },
  {
    "path": "build/check-versions.js",
    "content": "var chalk = require('chalk')\nvar semver = require('semver')\nvar packageConfig = require('../package.json')\nvar shell = require('shelljs')\nfunction exec (cmd) {\n  return require('child_process').execSync(cmd).toString().trim()\n}\n\nvar versionRequirements = [\n  {\n    name: 'node',\n    currentVersion: semver.clean(process.version),\n    versionRequirement: packageConfig.engines.node\n  },\n]\n\nif (shell.which('npm')) {\n  versionRequirements.push({\n    name: 'npm',\n    currentVersion: exec('npm --version'),\n    versionRequirement: packageConfig.engines.npm\n  })\n}\n\nmodule.exports = function () {\n  var warnings = []\n  for (var i = 0; i < versionRequirements.length; i++) {\n    var mod = versionRequirements[i]\n    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {\n      warnings.push(mod.name + ': ' +\n        chalk.red(mod.currentVersion) + ' should be ' +\n        chalk.green(mod.versionRequirement)\n      )\n    }\n  }\n\n  if (warnings.length) {\n    console.log('')\n    console.log(chalk.yellow('To use this template, you must update following to modules:'))\n    console.log()\n    for (var i = 0; i < warnings.length; i++) {\n      var warning = warnings[i]\n      console.log('  ' + warning)\n    }\n    console.log()\n    process.exit(1)\n  }\n}\n"
  },
  {
    "path": "build/dev-client.js",
    "content": "/* eslint-disable */\nrequire('eventsource-polyfill')\nvar hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')\n\nhotClient.subscribe(function (event) {\n  if (event.action === 'reload') {\n    window.location.reload()\n  }\n})\n"
  },
  {
    "path": "build/dev-server.js",
    "content": "require('./check-versions')()\n\nvar config = require('../config')\nif (!process.env.NODE_ENV) {\n  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)\n}\n\nvar opn = require('opn')\nvar path = require('path')\nvar express = require('express')\nvar webpack = require('webpack')\nvar proxyMiddleware = require('http-proxy-middleware')\nvar webpackConfig = require('./webpack.dev.conf')\n\n// default port where dev server listens for incoming traffic\nvar port = process.env.PORT || config.dev.port\n// automatically open browser, if not set will be false\nvar autoOpenBrowser = !!config.dev.autoOpenBrowser\n// Define HTTP proxies to your custom API backend\n// https://github.com/chimurai/http-proxy-middleware\nvar proxyTable = config.dev.proxyTable\n\nvar app = express()\nvar compiler = webpack(webpackConfig)\n\nvar devMiddleware = require('webpack-dev-middleware')(compiler, {\n  publicPath: webpackConfig.output.publicPath,\n  quiet: true\n})\n\nvar hotMiddleware = require('webpack-hot-middleware')(compiler, {\n  log: () => {}\n})\n// force page reload when html-webpack-plugin template changes\ncompiler.plugin('compilation', function (compilation) {\n  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {\n    hotMiddleware.publish({ action: 'reload' })\n    cb()\n  })\n})\n\n// proxy api requests\nObject.keys(proxyTable).forEach(function (context) {\n  var options = proxyTable[context]\n  if (typeof options === 'string') {\n    options = { target: options }\n  }\n  app.use(proxyMiddleware(options.filter || context, options))\n})\n\n// handle fallback for HTML5 history API\napp.use(require('connect-history-api-fallback')())\n\n// serve webpack bundle output\napp.use(devMiddleware)\n\n// enable hot-reload and state-preserving\n// compilation error display\napp.use(hotMiddleware)\n\n// serve pure static assets\nvar staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)\napp.use(staticPath, express.static('./static'))\n\nvar uri = 'http://localhost:' + port\n\nvar _resolve\nvar readyPromise = new Promise(resolve => {\n  _resolve = resolve\n})\n\nconsole.log('> Starting dev server...')\ndevMiddleware.waitUntilValid(() => {\n  console.log('> Listening at ' + uri + '\\n')\n  // when env is testing, don't need open it\n  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {\n    opn(uri)\n  }\n  _resolve()\n})\n\nvar server = app.listen(port)\n\nmodule.exports = {\n  ready: readyPromise,\n  close: () => {\n    server.close()\n  }\n}\n"
  },
  {
    "path": "build/utils.js",
    "content": "var path = require('path')\nvar config = require('../config')\nvar ExtractTextPlugin = require('extract-text-webpack-plugin')\n\nexports.assetsPath = function (_path) {\n  var assetsSubDirectory = process.env.NODE_ENV === 'production'\n    ? config.build.assetsSubDirectory\n    : config.dev.assetsSubDirectory\n  return path.posix.join(assetsSubDirectory, _path)\n}\n\nexports.cssLoaders = function (options) {\n  options = options || {}\n\n  var cssLoader = {\n    loader: 'css-loader',\n    options: {\n      minimize: process.env.NODE_ENV === 'production',\n      sourceMap: options.sourceMap\n    }\n  }\n\n  // generate loader string to be used with extract text plugin\n  function generateLoaders (loader, loaderOptions) {\n    var loaders = [cssLoader]\n    if (loader) {\n      loaders.push({\n        loader: loader + '-loader',\n        options: Object.assign({}, loaderOptions, {\n          sourceMap: options.sourceMap\n        })\n      })\n    }\n\n    // Extract CSS when that option is specified\n    // (which is the case during production build)\n    if (options.extract) {\n      return ExtractTextPlugin.extract({\n        use: loaders,\n        fallback: 'vue-style-loader',\n        publicPath: '../../'\n      })\n    } else {\n      return ['vue-style-loader'].concat(loaders)\n    }\n  }\n\n  // https://vue-loader.vuejs.org/en/configurations/extract-css.html\n  return {\n    css: generateLoaders(),\n    postcss: generateLoaders(),\n    less: generateLoaders('less'),\n    sass: generateLoaders('sass', { indentedSyntax: true }),\n    scss: generateLoaders('sass'),\n    stylus: generateLoaders('stylus'),\n    styl: generateLoaders('stylus')\n  }\n}\n\n// Generate loaders for standalone style files (outside of .vue)\nexports.styleLoaders = function (options) {\n  var output = []\n  var loaders = exports.cssLoaders(options)\n  for (var extension in loaders) {\n    var loader = loaders[extension]\n    output.push({\n      test: new RegExp('\\\\.' + extension + '$'),\n      use: loader\n    })\n  }\n  return output\n}\n"
  },
  {
    "path": "build/vue-loader.conf.js",
    "content": "var utils = require('./utils')\nvar config = require('../config')\nvar isProduction = process.env.NODE_ENV === 'production'\n\nmodule.exports = {\n  loaders: utils.cssLoaders({\n    sourceMap: isProduction\n      ? config.build.productionSourceMap\n      : config.dev.cssSourceMap,\n    extract: isProduction\n  })\n}\n"
  },
  {
    "path": "build/webpack.base.conf.js",
    "content": "var path = require('path')\nvar utils = require('./utils')\nvar config = require('../config')\nvar vueLoaderConfig = require('./vue-loader.conf')\n\nfunction resolve (dir) {\n  return path.join(__dirname, '..', dir)\n}\n\nmodule.exports = {\n  entry: {\n    app: './src/main.js'\n  },\n  output: {\n    path: config.build.assetsRoot,\n    filename: '[name].js',\n    publicPath: process.env.NODE_ENV === 'production'\n      ? config.build.assetsPublicPath\n      : config.dev.assetsPublicPath\n  },\n  resolve: {\n    extensions: ['.js', '.vue', '.json'],\n    alias: {\n      'vue$': 'vue/dist/vue.esm.js',\n      '@': resolve('src')\n    }\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: vueLoaderConfig\n      },\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        include: [resolve('src'), resolve('test'), resolve('/node_modules/webrtc-adapter/src')]\n      },\n      {\n        test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/,\n        loader: 'url-loader',\n        options: {\n          limit: 10000,\n          name: utils.assetsPath('img/[name].[hash:7].[ext]')\n        }\n      },\n      {\n        test: /\\.(woff2?|eot|ttf|otf)(\\?.*)?$/,\n        loader: 'url-loader',\n        options: {\n          limit: 10000,\n          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "build/webpack.dev.conf.js",
    "content": "var utils = require('./utils')\nvar webpack = require('webpack')\nvar config = require('../config')\nvar merge = require('webpack-merge')\nvar baseWebpackConfig = require('./webpack.base.conf')\nvar HtmlWebpackPlugin = require('html-webpack-plugin')\nvar FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')\nvar path = require('path');\n\n// add hot-reload related code to entry chunks\nObject.keys(baseWebpackConfig.entry).forEach(function (name) {\n  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])\n})\n\nmodule.exports = merge(baseWebpackConfig, {\n  module: {\n    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })\n  },\n  // cheap-module-eval-source-map is faster for development\n  devtool: '#cheap-module-eval-source-map',\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': config.dev.env\n    }),\n    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage\n    new webpack.HotModuleReplacementPlugin(),\n    new webpack.NoEmitOnErrorsPlugin(),\n    // https://github.com/ampedandwired/html-webpack-plugin\n    new HtmlWebpackPlugin({\n      filename: 'index.html',\n      template: 'index.html',\n      inject: true,\n      favicon: path.resolve('favicon.ico'),\n    }),\n    new FriendlyErrorsPlugin()\n  ]\n})\n"
  },
  {
    "path": "build/webpack.prod.conf.js",
    "content": "var path = require('path')\nvar utils = require('./utils')\nvar webpack = require('webpack')\nvar config = require('../config')\nvar merge = require('webpack-merge')\nvar baseWebpackConfig = require('./webpack.base.conf')\nvar CopyWebpackPlugin = require('copy-webpack-plugin')\nvar HtmlWebpackPlugin = require('html-webpack-plugin')\nvar ExtractTextPlugin = require('extract-text-webpack-plugin')\nvar OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')\n\nvar env = config.build.env\n\nvar webpackConfig = merge(baseWebpackConfig, {\n  module: {\n    rules: utils.styleLoaders({\n      sourceMap: config.build.productionSourceMap,\n      extract: true\n    })\n  },\n  devtool: config.build.productionSourceMap ? '#source-map' : false,\n  output: {\n    path: config.build.assetsRoot,\n    filename: utils.assetsPath('js/[name].[chunkhash].js'),\n    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')\n  },\n  plugins: [\n    // http://vuejs.github.io/vue-loader/en/workflow/production.html\n    new webpack.DefinePlugin({\n      'process.env': env\n    }),\n    new webpack.optimize.UglifyJsPlugin({\n      compress: {\n        warnings: false\n      },\n      sourceMap: true\n    }),\n    // extract css into its own file\n    new ExtractTextPlugin({\n      filename: utils.assetsPath('css/[name].[contenthash].css')\n    }),\n    // Compress extracted CSS. We are using this plugin so that possible\n    // duplicated CSS from different components can be deduped.\n    new OptimizeCSSPlugin({\n      cssProcessorOptions: {\n        safe: true\n      }\n    }),\n    // generate dist index.html with correct asset hash for caching.\n    // you can customize output by editing /index.html\n    // see https://github.com/ampedandwired/html-webpack-plugin\n    new HtmlWebpackPlugin({\n      filename: config.build.index,\n      template: 'index.html',\n      inject: true,\n      favicon: path.resolve('favicon.ico'),\n      minify: {\n        removeComments: true,\n        collapseWhitespace: true,\n        removeAttributeQuotes: true\n        // more options:\n        // https://github.com/kangax/html-minifier#options-quick-reference\n      },\n      // necessary to consistently work with multiple chunks via CommonsChunkPlugin\n      chunksSortMode: 'dependency'\n    }),\n    // split vendor js into its own file\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'vendor',\n      minChunks: function (module, count) {\n        // any required modules inside node_modules are extracted to vendor\n        return (\n          module.resource &&\n          /\\.js$/.test(module.resource) &&\n          module.resource.indexOf(\n            path.join(__dirname, '../node_modules')\n          ) === 0\n        )\n      }\n    }),\n    // extract webpack runtime and module manifest to its own file in order to\n    // prevent vendor hash from being updated whenever app bundle is updated\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'manifest',\n      chunks: ['vendor']\n    }),\n    // copy custom static assets\n    new CopyWebpackPlugin([\n      {\n        from: path.resolve(__dirname, '../static'),\n        to: config.build.assetsSubDirectory,\n        ignore: ['.*']\n      }\n    ])\n  ]\n})\n\nif (config.build.productionGzip) {\n  var CompressionWebpackPlugin = require('compression-webpack-plugin')\n\n  webpackConfig.plugins.push(\n    new CompressionWebpackPlugin({\n      asset: '[path].gz[query]',\n      algorithm: 'gzip',\n      test: new RegExp(\n        '\\\\.(' +\n        config.build.productionGzipExtensions.join('|') +\n        ')$'\n      ),\n      threshold: 10240,\n      minRatio: 0.8\n    })\n  )\n}\n\nif (config.build.bundleAnalyzerReport) {\n  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin\n  webpackConfig.plugins.push(new BundleAnalyzerPlugin())\n}\n\nmodule.exports = webpackConfig\n"
  },
  {
    "path": "config/dev.env.js",
    "content": "var merge = require('webpack-merge')\nvar prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEnv, {\n  NODE_ENV: '\"development\"'\n})\n"
  },
  {
    "path": "config/index.js",
    "content": "// see http://vuejs-templates.github.io/webpack for documentation.\nvar path = require('path')\n\nmodule.exports = {\n  build: {\n    env: require('./prod.env'),\n    index: path.resolve(__dirname, '../dist/index.html'),\n    assetsRoot: path.resolve(__dirname, '../dist'),\n    assetsSubDirectory: 'static',\n    assetsPublicPath: '/',\n    productionSourceMap: true,\n    // Gzip off by default as many popular static hosts such as\n    // Surge or Netlify already gzip all static assets for you.\n    // Before setting to `true`, make sure to:\n    // npm install --save-dev compression-webpack-plugin\n    productionGzip: false,\n    productionGzipExtensions: ['js', 'css'],\n    // Run the build command with an extra argument to\n    // View the bundle analyzer report after build finishes:\n    // `npm run build --report`\n    // Set to `true` or `false` to always turn it on or off\n    bundleAnalyzerReport: process.env.npm_config_report\n  },\n  dev: {\n    env: require('./dev.env'),\n    port: 9080,\n    autoOpenBrowser: true,\n    assetsSubDirectory: 'static',\n    assetsPublicPath: '/',\n    proxyTable: {},\n    // CSS Sourcemaps off by default because relative paths are \"buggy\"\n    // with this option, according to the CSS-Loader README\n    // (https://github.com/webpack/css-loader#sourcemaps)\n    // In our experience, they generally work as expected,\n    // just be aware of this issue when enabling this option.\n    cssSourceMap: false\n  }\n}\n"
  },
  {
    "path": "config/prod.env.js",
    "content": "module.exports = {\n  NODE_ENV: '\"production\"'\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>飞享IM</title>\n    <link rel=\"stylesheet\" href=\"static/css/reset.css\">\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "index.md",
    "content": "\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"wechat\",\n  \"version\": \"1.1.6\",\n  \"description\": \"基于fshare的开源即时通讯web客户端\",\n  \"author\": \"comsince\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"node build/dev-server.js\",\n    \"start\": \"node build/dev-server.js\",\n    \"build\": \"node build/build.js\"\n  },\n  \"dependencies\": {\n    \"@wcjiang/notify\": \"^2.0.12\",\n    \"axios\": \"^0.19.0\",\n    \"crypto-js\": \"^3.1.9-1\",\n    \"element-ui\": \"^2.13.0\",\n    \"kurento-utils\": \"^6.14.0\",\n    \"pinyin\": \"^2.9.0\",\n    \"qiniu-js\": \"^2.5.5\",\n    \"stylus\": \"^0.54.5\",\n    \"stylus-loader\": \"^3.0.1\",\n    \"uuid-js\": \"^0.7.5\",\n    \"v-viewer\": \"^1.5.1\",\n    \"vue\": \"^2.5.2\",\n    \"vue-axios\": \"^2.1.4\",\n    \"vue-router\": \"^3.0.1\",\n    \"vuex\": \"^3.0.1\",\n    \"webrtc-adapter\": \"^7.5.1\",\n    \"xgplayer\": \"^2.4.7\",\n    \"xgplayer-vue\": \"^1.1.5\"\n  },\n  \"devDependencies\": {\n    \"autoprefixer\": \"^6.7.2\",\n    \"babel-core\": \"^6.22.1\",\n    \"babel-loader\": \"^6.2.10\",\n    \"babel-plugin-transform-runtime\": \"^6.22.0\",\n    \"babel-preset-env\": \"^1.3.2\",\n    \"babel-preset-stage-2\": \"^6.22.0\",\n    \"babel-register\": \"^6.22.0\",\n    \"chalk\": \"^1.1.3\",\n    \"connect-history-api-fallback\": \"^1.3.0\",\n    \"copy-webpack-plugin\": \"^4.0.1\",\n    \"css-loader\": \"^0.28.0\",\n    \"eventsource-polyfill\": \"^0.9.6\",\n    \"express\": \"^4.14.1\",\n    \"extract-text-webpack-plugin\": \"^2.0.0\",\n    \"file-loader\": \"^0.11.1\",\n    \"friendly-errors-webpack-plugin\": \"^1.1.3\",\n    \"html-webpack-plugin\": \"^2.28.0\",\n    \"http-proxy-middleware\": \"^0.17.3\",\n    \"webpack-bundle-analyzer\": \"^2.2.1\",\n    \"semver\": \"^5.3.0\",\n    \"shelljs\": \"^0.7.6\",\n    \"opn\": \"^4.0.2\",\n    \"optimize-css-assets-webpack-plugin\": \"^1.3.0\",\n    \"ora\": \"^1.2.0\",\n    \"rimraf\": \"^2.6.0\",\n    \"url-loader\": \"^0.5.8\",\n    \"vue-loader\": \"^11.3.4\",\n    \"vue-style-loader\": \"^2.0.5\",\n    \"vue-template-compiler\": \"^2.2.6\",\n    \"webpack\": \"^2.3.3\",\n    \"webpack-dev-middleware\": \"^1.10.0\",\n    \"webpack-hot-middleware\": \"^2.18.0\",\n    \"webpack-merge\": \"^4.1.0\"\n  },\n  \"engines\": {\n    \"node\": \">= 4.0.0\",\n    \"npm\": \">= 3.0.0\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 8\"\n  ]\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <div class=\"layout-container\" id=\"app-chat\">\n    <router-view></router-view>\n    <searchfriend></searchfriend>\n    <creategroup></creategroup>\n    <relayMessage></relayMessage>\n  </div>\n</template>\n\n<script>\nimport { mapActions } from 'vuex'\nimport searchfriend from './page/friend/searchfriend'\nimport creategroup from './page/group/creategroup'\nimport relayMessage from './components/menu/relayMessage'\nexport default {\n   name : 'AppChat',\n   components: {\n     searchfriend,\n     creategroup,\n     relayMessage,\n   }\n}\n</script>\n\n<style scoped>\n.layout-container {\n    width: 100%;\n    height: 100%;\n    min-width: 1088px;\n    min-height: 550px;\n    box-sizing: border-box;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column\n}\n\n</style>\n\n\n"
  },
  {
    "path": "src/assets/fonts/iconfont.css",
    "content": "@font-face {font-family: \"iconfont\";\n  src: url('iconfont.eot?t=1592274269382'); /* IE9 */\n  src: url('iconfont.eot?t=1592274269382#iefix') format('embedded-opentype'), /* IE6-IE8 */\n  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'),\n  url('iconfont.woff?t=1592274269382') format('woff'),\n  url('iconfont.ttf?t=1592274269382') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */\n  url('iconfont.svg?t=1592274269382#iconfont') format('svg'); /* iOS 4.1- */\n}\n\n.iconfont {\n  font-family: \"iconfont\" !important;\n  font-size: 16px;\n  font-style: normal;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.icon-shipin:before {\n  content: \"\\e669\";\n}\n\n.icon-shanchu-fangkuang:before {\n  content: \"\\e791\";\n}\n\n.icon-zengjiashuzi:before {\n  content: \"\\e641\";\n}\n\n.icon-jianshanchu-xianxingfangkuang:before {\n  content: \"\\e75a\";\n}\n\n.icon-zengjia2:before {\n  content: \"\\e689\";\n}\n\n.icon-yichu:before {\n  content: \"\\e656\";\n}\n\n.icon-zengjia:before {\n  content: \"\\e668\";\n}\n\n.icon-delete-br:before {\n  content: \"\\e63d\";\n}\n\n.icon-loading-solid:before {\n  content: \"\\e647\";\n}\n\n.icon-fasongshibai:before {\n  content: \"\\e62c\";\n}\n\n.icon-pengyou1:before {\n  content: \"\\e631\";\n}\n\n.icon-yaoqinghaoyou:before {\n  content: \"\\e61c\";\n}\n\n.icon-jiahaoyou:before {\n  content: \"\\e603\";\n}\n\n.icon-ai-video:before {\n  content: \"\\e66b\";\n}\n\n.icon-biaoqing:before {\n  content: \"\\e666\";\n}\n\n.icon-dkw_xiaoxi:before {\n  content: \"\\e606\";\n}\n\n.icon-dianhua:before {\n  content: \"\\e729\";\n}\n\n.icon-pengyou:before {\n  content: \"\\e61a\";\n}\n\n.icon-sousuo:before {\n  content: \"\\e659\";\n}\n\n.icon-tuichu:before {\n  content: \"\\e61f\";\n}\n\n.icon-tupian:before {\n  content: \"\\e623\";\n}\n\n.icon-wenjian:before {\n  content: \"\\e61b\";\n}\n\n.icon-guaduan:before {\n  content: \"\\e640\";\n}\n\n"
  },
  {
    "path": "src/assets/fonts/iconfont.js",
    "content": "!function(c){var t,i,o,a,l,h,e,s='<svg><symbol id=\"icon-shipin\" viewBox=\"0 0 1024 1024\"><path d=\"M812.032 102.4H211.968c-60.416 0-109.056 49.152-109.056 110.08v495.104c0 60.928 48.64 110.08 109.056 110.08h600.064c60.416 0 109.056-49.152 109.056-110.08V212.48c0-60.928-48.64-110.08-109.056-110.08zM689.664 483.84L416.768 648.192c-4.608 2.56-9.216 4.096-13.824 4.096-4.608 0-9.216-1.024-13.312-3.584-8.704-4.608-13.824-13.824-13.824-24.064V295.424c0-9.728 5.12-18.944 13.824-24.064s18.944-4.608 27.648 0.512l272.896 164.352c8.192 5.12 13.312 13.824 13.312 23.552-0.512 10.24-5.632 18.944-13.824 24.064z m-535.04 385.024h715.264V921.6H154.624z\" fill=\"#424242\" ></path></symbol><symbol id=\"icon-shanchu-fangkuang\" viewBox=\"0 0 1024 1024\"><path d=\"M884.363636 46.545455c51.2 0 93.090909 41.890909 93.090909 93.090909v744.727272c0 51.2-41.890909 93.090909-93.090909 93.090909H139.636364c-51.2 0-93.090909-41.890909-93.090909-93.090909V139.636364c0-51.2 41.890909-93.090909 93.090909-93.090909h744.727272z m23.272728 837.818181V139.636364c0-12.613818-10.658909-23.272727-23.272728-23.272728H139.636364c-12.613818 0-23.272727 10.658909-23.272728 23.272728v744.727272c0 12.613818 10.658909 23.272727 23.272728 23.272728h744.727272c12.613818 0 23.272727-10.658909 23.272728-23.272728z m-151.272728-407.272727h-488.727272a34.909091 34.909091 0 1 0 0 69.818182h488.727272a34.909091 34.909091 0 1 0 0-69.818182\" fill=\"#797979\" ></path></symbol><symbol id=\"icon-zengjiashuzi\" viewBox=\"0 0 1024 1024\"><path d=\"M823.8 91H194.2C137.3 91 91 137.3 91 194.2v629.6C91 880.7 137.3 927 194.2 927h629.6c56.9 0 103.2-46.3 103.2-103.2V194.2C927 137.3 880.7 91 823.8 91z m41.3 732.8c0 22.8-18.5 41.3-41.3 41.3H194.2c-22.8 0-41.3-18.5-41.3-41.3V194.2c0-22.8 18.5-41.3 41.3-41.3h629.6c22.8 0 41.3 18.5 41.3 41.3v629.6z\"  ></path><path d=\"M710.3 478H540V307.7c0-17.1-13.9-31-31-31s-31 13.9-31 31V478H307.7c-17.1 0-31 13.9-31 31s13.9 31 31 31H478v170.3c0 17.1 13.9 31 31 31s31-13.9 31-31V540h170.3c17.1 0 31-13.9 31-31s-13.9-31-31-31z\"  ></path></symbol><symbol id=\"icon-jianshanchu-xianxingfangkuang\" viewBox=\"0 0 1024 1024\"><path d=\"M129.5 894.5v-765h765v765h-765z m700-65v-635h-635v635h635z\" fill=\"\" ></path><path d=\"M862.5 862.5h-701v-701h701v701z m-700-1h699v-699h-699v699z\" fill=\"\" ></path><path d=\"M288 480h448c17.5 0 32 14.4 32 32 0 17.5-14.4 32-32 32H288c-17.5 0-32-14.4-32-32s14.4-32 32-32z\" fill=\"\" ></path></symbol><symbol id=\"icon-zengjia2\" viewBox=\"0 0 1024 1024\"><path d=\"M473.60158 550.400226v230.397742c0 21.206823 17.186178 38.402032 38.391195 38.402032 21.21766 0 38.405644-17.197016 38.405645-38.402032V550.400226h230.399548c21.205016 0 38.402032-17.198822 38.402032-38.405645 0-21.205016-17.197016-38.391195-38.402032-38.391194H550.39842V243.205644c0-21.219466-17.187984-38.405644-38.405645-38.405644-21.205016 0-38.391195 17.186178-38.391195 38.405644v230.397743H243.193001c-21.205016 0-38.393001 17.187984-38.393001 38.391194 0 21.205016 17.187984 38.405644 38.393001 38.405645h230.408579z\" fill=\"#231815\" ></path><path d=\"M0 921.594581V102.394581C0 45.838238 45.838238 0 102.394581 0h819.2c56.554538 0 102.405419 45.838238 102.405419 102.394581v819.2c0 56.556344-45.852687 102.405419-102.405419 102.405419H102.394581C45.838238 1024 0 978.150925 0 921.594581z m947.203161 0V102.394581c0-18.098319-7.499423-25.597742-25.60858-25.597742H102.394581c-18.096513 0-25.597742 7.499423-25.597742 25.597742v819.2c0 18.109156 7.499423 25.610386 25.597742 25.610386h819.2c18.10735 0 25.60858-7.501229 25.60858-25.610386z\" fill=\"#231815\" ></path></symbol><symbol id=\"icon-yichu\" viewBox=\"0 0 1024 1024\"><path d=\"M981.475083 0H42.524917a42.524917 42.524917 0 0 0-42.524917 42.524917v935.548173a42.524917 42.524917 0 0 0 42.524917 42.524917h935.548173a42.524917 42.524917 0 0 0 42.524917-42.524917V42.524917a42.524917 42.524917 0 0 0-39.122924-42.524917zM935.548173 935.548173H85.049834V85.049834h850.498339z\" fill=\"#666666\" ></path><path d=\"M340.199336 554.524917h340.199335a42.524917 42.524917 0 0 0 42.524917-42.524917 41.674419 41.674419 0 0 0-5.953488-20.41196 42.524917 42.524917 0 0 0-36.571429-26.365449H340.199336a42.524917 42.524917 0 0 0-42.524917 42.524917 41.674419 41.674419 0 0 0 5.953488 20.41196 42.524917 42.524917 0 0 0 36.571429 26.365449z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-zengjia\" viewBox=\"0 0 1024 1024\"><path d=\"M66.56 66.56v890.88h890.88V66.56H66.56z m827.24352 827.24352H130.18624V130.19648h763.6224v763.60704z\" fill=\"#333333\" ></path><path d=\"M480.23552 480.25088H311.13728c-17.5616 0-32.01024 14.208-32.01024 31.74912 0 17.65888 14.33088 31.75936 32.01024 31.75936h169.09824v169.09824c0 17.5616 14.22336 32.01024 31.75936 32.01024 17.664 0 31.75936-14.33088 31.75936-32.01024v-169.09824h169.10848c17.5616 0 32-14.21824 32-31.75936 0-17.65888-14.32064-31.74912-32-31.74912h-169.10848V311.1424c0-17.56672-14.21312-32.02048-31.75936-32.02048-17.65376 0-31.75936 14.34112-31.75936 32.02048v169.10848z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-delete-br\" viewBox=\"0 0 1024 1024\"><path d=\"M22.286336 78.303232v865.705984h983.525376V78.303232H22.286336z m942.563328 824.745984H63.246336V119.263232h901.605376v783.785984z\" fill=\"\" ></path><path d=\"M316.83584 510.312448h394.422272v40.96H316.83584z\" fill=\"\" ></path></symbol><symbol id=\"icon-loading-solid\" viewBox=\"0 0 1024 1024\"><path d=\"M512 0a51.2 51.2 0 0 1 0 102.4C285.7728 102.4 102.4 285.7728 102.4 512s183.3728 409.6 409.6 409.6 409.6-183.3728 409.6-409.6c0-82.6112-24.448-161.4592-69.5808-228.4544A51.2 51.2 0 0 1 936.96 226.3552 509.7984 509.7984 0 0 1 1024 512c0 282.7776-229.2224 512-512 512S0 794.7776 0 512 229.2224 0 512 0z\"  ></path></symbol><symbol id=\"icon-fasongshibai\" viewBox=\"0 0 1024 1024\"><path d=\"M512 0C229.052632 0 0 229.052632 0 512s229.052632 512 512 512 512-229.052632 512-512S794.947368 0 512 0z m0 808.421053c-29.642105 0-53.894737-24.252632-53.894737-53.894737s24.252632-53.894737 53.894737-53.894737 53.894737 24.252632 53.894737 53.894737-24.252632 53.894737-53.894737 53.894737z m26.947368-202.105264h-53.894736L458.105263 215.578947h107.789474l-26.947369 390.736842z\" fill=\"#E93A0F\" ></path></symbol><symbol id=\"icon-pengyou1\" viewBox=\"0 0 1024 1024\"><path d=\"M633.6 537.6c64-38.4 102.4-102.4 102.4-172.8 0-115.2-96-204.8-217.6-204.8S300.8 249.6 300.8 364.8c0 70.4 38.4 134.4 102.4 172.8-140.8 44.8-224 166.4-224 294.4 0 12.8 12.8 32 32 32 12.8 0 32-12.8 32-32-6.4-140.8 115.2-256 268.8-256 153.6 0 275.2 115.2 275.2 262.4 0 12.8 12.8 32 32 32 12.8 0 32-12.8 32-32-6.4-134.4-89.6-256-217.6-300.8z m38.4-172.8C672 448 601.6 518.4 512 518.4S352 448 352 364.8c0-83.2 70.4-153.6 160-153.6 89.6 6.4 160 70.4 160 153.6z m0 0\"  ></path></symbol><symbol id=\"icon-yaoqinghaoyou\" viewBox=\"0 0 1024 1024\"><path d=\"M1000.727273 837.818182h-116.363637v-116.363637c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727v116.363637h-116.363637c-13.963636 0-23.272727 9.309091-23.272727 23.272727s9.309091 23.272727 23.272727 23.272727h116.363637v116.363637c0 13.963636 9.309091 23.272727 23.272727 23.272727s23.272727-9.309091 23.272727-23.272727v-116.363637h116.363637c13.963636 0 23.272727-9.309091 23.272727-23.272727s-11.636364-23.272727-23.272727-23.272727z m-172.218182-190.836364c6.981818-9.309091 4.654545-25.6-4.654546-32.581818-55.854545-41.890909-118.690909-72.145455-183.854545-88.436364 90.763636-46.545455 151.272727-139.636364 151.272727-246.690909 0-153.6-125.672727-279.272727-279.272727-279.272727S232.727273 125.672727 232.727273 279.272727c0 109.381818 62.836364 202.472727 153.6 249.018182C172.218182 584.145455 11.636364 770.327273 0 998.4c0 13.963636 9.309091 23.272727 23.272727 23.272727h2.327273c11.636364 0 23.272727-9.309091 23.272727-23.272727 11.636364-249.018182 214.109091-442.181818 465.454546-442.181818 102.4 0 202.472727 32.581818 283.927272 93.090909 9.309091 9.309091 23.272727 6.981818 30.254546-2.327273zM279.272727 279.272727c0-128 104.727273-232.727273 232.727273-232.727272s232.727273 104.727273 232.727273 232.727272-104.727273 232.727273-232.727273 232.727273-232.727273-104.727273-232.727273-232.727273z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-jiahaoyou\" viewBox=\"0 0 1024 1024\"><path d=\"M438 526.7c-3.7 0-7.4-0.6-11.1-2-47.5-17.1-87.6-48.3-116-90.2-35.6-52.5-48.6-115.8-36.6-178s47.5-116.2 100-151.8C426.8 69.1 490 56 552.3 68.1c62.3 12 116.2 47.5 151.8 100 35.6 52.5 48.6 115.8 36.6 178-12 62.3-47.5 116.2-100 151.8-16 10.8-33.2 19.7-51.2 26.3-17 6.3-35.8-2.4-42.1-19.4-6.3-17 2.4-35.8 19.4-42.1 13-4.8 25.5-11.2 37.1-19.1 38.1-25.8 63.8-64.8 72.5-109.9 8.7-45.1-0.8-90.9-26.5-128.9-25.8-38-64.8-63.8-109.9-72.4-45-8.6-90.9 0.7-128.9 26.5-78.5 53.2-99.1 160.3-45.9 238.8 20.6 30.4 49.6 52.9 84 65.3 17 6.1 25.9 24.9 19.7 41.9-4.9 13.5-17.5 21.8-30.9 21.8zM130.8 952c-18.1 0-32.8-14.7-32.8-32.8 0-164.7 71-361.1 409.5-361.1 62.8 0 119.1 6.7 167.3 19.9 17.4 4.8 27.7 22.8 22.9 40.2-4.8 17.4-22.8 27.7-40.2 22.9-42.6-11.6-93.1-17.6-150-17.6-234.7 0-343.9 93.9-343.9 295.6-0.1 18.2-14.7 32.9-32.8 32.9zM643.7 753h314.1v98.3H643.7z\" fill=\"#009FE8\" ></path><path d=\"M751.6 645.1h98.3v314.1h-98.3z\" fill=\"#009FE8\" ></path></symbol><symbol id=\"icon-ai-video\" viewBox=\"0 0 1024 1024\"><path d=\"M647.725 840.897c0 0 93.091 0 93.091-93.091L740.816 282.353c0-93.091-93.091-93.091-93.091-93.091L93.091 189.262c0 0-93.091 0-93.091 93.091l0 465.453c0 93.091 93.091 93.091 93.091 93.091L647.725 840.897zM795.041 371.115 795.041 660.72l228.956 180.177L1023.997 189.263 795.041 371.115z\"  ></path></symbol><symbol id=\"icon-biaoqing\" viewBox=\"0 0 1024 1024\"><path d=\"M510.54537 60.915371c-248.305249 0-449.610044 201.296609-449.610044 449.610044s201.304795 449.609021 449.610044 449.609021c248.304226 0 449.610044-201.295586 449.610044-449.609021S758.849596 60.915371 510.54537 60.915371zM236.869291 393.236106c0-43.181475 35.011398-78.192873 78.192873-78.192873 43.182498 0 78.192873 35.011398 78.192873 78.192873s-35.010375 78.192873-78.192873 78.192873C271.879666 471.428979 236.869291 436.417581 236.869291 393.236106zM510.54537 823.296909c-119.408577 0-217.989803-89.227184-232.689596-204.644867-1.221828-3.225461-1.89107-6.729262-1.89107-10.384511 0-16.198937 13.134135-29.321816 29.322839-29.321816 16.187681 0 29.321816 13.122879 29.321816 29.321816l1.069355 0c9.735735 87.966471 84.302011 156.385746 174.865633 156.385746 89.035826 0 162.608471-66.129116 174.312024-151.948691-0.210801-1.449002-0.324388-2.929726-0.324388-4.437056 0-16.198937 13.114692-29.321816 29.321816-29.321816 16.187681 0 29.321816 13.122879 29.321816 29.321816l1.146103 0C734.395648 728.676901 633.523239 823.296909 510.54537 823.296909zM706.027553 471.428979c-43.181475 0-78.192873-35.011398-78.192873-78.192873s35.011398-78.192873 78.192873-78.192873 78.192873 35.011398 78.192873 78.192873S749.209028 471.428979 706.027553 471.428979z\"  ></path></symbol><symbol id=\"icon-dkw_xiaoxi\" viewBox=\"0 0 1024 1024\"><path d=\"M288 426.666667a53.333333 53.333333 0 1 0 53.333333 53.333333 53.333333 53.333333 0 0 0-53.333333-53.333333z m0 64a10.666667 10.666667 0 1 1 10.666667-10.666667 10.666667 10.666667 0 0 1-10.666667 10.666667zM522.666667 426.666667a53.333333 53.333333 0 1 0 53.333333 53.333333 53.333333 53.333333 0 0 0-53.333333-53.333333z m0 64a10.666667 10.666667 0 1 1 10.666666-10.666667 10.666667 10.666667 0 0 1-10.666666 10.666667zM757.333333 426.666667a53.333333 53.333333 0 1 0 53.333334 53.333333 53.333333 53.333333 0 0 0-53.333334-53.333333z m0 64a10.666667 10.666667 0 1 1 10.666667-10.666667 10.666667 10.666667 0 0 1-10.666667 10.666667z\" fill=\"#666666\" ></path><path d=\"M512 42.666667C224.853333 42.666667 0 229.333333 0 469.333333a413.44 413.44 0 0 0 166.826667 327.04l-42.666667 126.72a43.946667 43.946667 0 0 0 0 50.773334 29.653333 29.653333 0 0 0 21.333333 8.533333 97.066667 97.066667 0 0 0 42.666667-14.506667c31.146667-17.066667 66.986667-35.626667 95.786667-50.773333L322.133333 896a673.066667 673.066667 0 0 0 189.866667 21.333333c282.24 0 512-200.96 512-448C1024 229.333333 799.146667 42.666667 512 42.666667z m0 832a604.586667 604.586667 0 0 1-185.173333-21.333334 21.333333 21.333333 0 0 0-17.28 1.493334c-9.6 5.333333-25.813333 13.653333-45.226667 23.893333-29.226667 15.146667-65.28 33.92-96.853333 51.2l-2.346667 1.28 46.72-135.893333a21.333333 21.333333 0 0 0-7.68-24.32A373.12 373.12 0 0 1 42.666667 469.333333C42.666667 253.226667 248.746667 85.333333 512 85.333333s469.333333 168.746667 469.333333 384c0 222.72-210.56 405.333333-469.333333 405.333334z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-dianhua\" viewBox=\"0 0 1024 1024\"><path d=\"M418.368 344.192l-88.416 89.92C376 536.32 473.632 650.144 586.912 688.416l76.416-76.192s181.184 31.616 221.92 51.392c35.296 17.12 46.976 55.072 41.44 101.888-5.92 50.304-71.84 161.248-190.624 162.464-118.784 1.216-293.696-88.256-419.008-220.288C191.744 575.616 80 408.288 97.92 266.144c17.888-142.08 141.696-167.52 172.672-169.792 31.296-2.304 56.16 6.88 75.04 30.272 24.32 30.144 72.768 217.568 72.768 217.568z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-pengyou\" viewBox=\"0 0 1024 1024\"><path d=\"M572.2 530c-114.3 0-207.3-93-207.3-207.3s93-207.3 207.3-207.3c114.3 0 207.3 93 207.3 207.3S686.5 530 572.2 530z m0-364.7c-86.8 0-157.3 70.6-157.3 157.3S485.4 480 572.2 480s157.3-70.6 157.3-157.3S659 165.3 572.2 165.3zM279.1 515.8c-3 0-6-0.5-9-1.7-35.4-13.7-65.7-37.5-87.5-68.7-22.4-32-34.2-69.7-34.2-108.8 0-104.9 85.4-190.3 190.3-190.3 13.8 0 25 11.2 25 25s-11.2 25-25 25c-77.4 0-140.3 62.9-140.3 140.3 0 57.6 36.1 110.2 89.7 130.9 12.9 5 19.3 19.5 14.3 32.3-3.8 9.9-13.3 16-23.3 16zM932 888.6c-13.8 0-25-11.2-25-25 0-106.8-49.2-204.8-135-268.7-11.1-8.2-13.4-23.9-5.1-35 8.2-11.1 23.9-13.4 35-5.1C849 589.9 888 636 914.7 688.1c28.1 54.6 42.3 113.7 42.3 175.5 0 13.8-11.2 25-25 25z\"  ></path><path d=\"M92 784.5c-1 0-2.1-0.1-3.1-0.2-13.7-1.7-23.4-14.2-21.7-27.9 7.6-61.4 29-118.2 63.5-169 33-48.4 77.3-89.3 128.4-118.4 12-6.8 27.3-2.6 34.1 9.4 6.8 12 2.6 27.3-9.4 34.1-93 52.9-153.9 144-167 250-1.6 12.7-12.3 22-24.8 22zM212.4 889.8c-13.8 0-25-11.2-25-25 0-51.9 10.2-102.3 30.3-149.8 19.4-45.8 47.1-87 82.5-122.3 35.3-35.3 76.5-63.1 122.3-82.5 47.5-20.1 97.9-30.3 149.8-30.3 13.8 0 25 11.2 25 25s-11.2 25-25 25c-184.6 0-334.8 150.2-334.8 334.8-0.1 13.9-11.3 25.1-25.1 25.1z\"  ></path></symbol><symbol id=\"icon-sousuo\" viewBox=\"0 0 1024 1024\"><path d=\"M883.2 864C970.666667 772.266667 1024 648.533333 1024 512 1024 230.4 793.6 0 512 0S0 230.4 0 512s230.4 512 512 512c130.133333 0 249.6-49.066667 341.333333-130.133333l123.733334 123.733333c6.4 6.4 19.2 6.4 25.6 0l4.266666-4.266667c6.4-6.4 6.4-19.2 0-25.6l-123.733333-123.733333zM512 981.333333C253.866667 981.333333 42.666667 770.133333 42.666667 512S253.866667 42.666667 512 42.666667s469.333333 211.2 469.333333 469.333333-211.2 469.333333-469.333333 469.333333z\"  ></path></symbol><symbol id=\"icon-tuichu\" viewBox=\"0 0 1024 1024\"><path d=\"M696.4 926H237.3c-67.5 0-122.4-54.9-122.4-122.4V221c0-67.5 54.9-122.4 122.4-122.4h459.1c67.5 0 122.4 54.9 122.4 122.4v67.6c0 14-11.3 25.3-25.3 25.3s-25.3-11.3-25.3-25.3V221c0-39.6-32.2-71.8-71.8-71.8H237.3c-39.6 0-71.8 32.2-71.8 71.8v582.7c0 39.6 32.2 71.8 71.8 71.8h459.1c39.6 0 71.8-32.2 71.8-71.8v-85.2c0-14 11.3-25.3 25.3-25.3s25.3 11.3 25.3 25.3v85.2c0 67.4-54.9 122.3-122.4 122.3z\" fill=\"#383838\" ></path><path d=\"M734.2 685.1c-6.5 0-12.9-2.5-17.9-7.4-9.9-9.9-9.9-25.9 0-35.8L846 512.3 716.3 382.6c-9.9-9.9-9.9-25.9 0-35.8 9.9-9.9 25.9-9.9 35.8 0l147.6 147.6c9.9 9.9 9.9 25.9 0 35.8L752.1 677.7c-5 5-11.4 7.4-17.9 7.4z\" fill=\"#383838\" ></path><path d=\"M876.7 537.6h-507c-14 0-25.3-11.3-25.3-25.3s11.3-25.3 25.3-25.3h507c14 0 25.3 11.3 25.3 25.3s-11.3 25.3-25.3 25.3z\" fill=\"#383838\" ></path></symbol><symbol id=\"icon-tupian\" viewBox=\"0 0 1024 1024\"><path d=\"M769.3 154.8H258.7c-92.9 0-168.5 75.6-168.5 168.5v379.8c0 92.9 75.6 168.5 168.5 168.5h510.5c92.9 0 168.5-75.6 168.5-168.5V323.3c0.1-92.9-75.5-168.5-168.4-168.5z m-372 146c48.4 0 87.7 39.2 87.7 87.7 0 48.4-39.2 87.7-87.7 87.7-48.4 0-87.7-39.2-87.7-87.7 0.1-48.4 39.3-87.7 87.7-87.7z m-117 407l120.2-187.3 85.8 121L606.6 460l154.5 247.8H280.3z\"  ></path></symbol><symbol id=\"icon-wenjian\" viewBox=\"0 0 1024 1024\"><path d=\"M928.426667 354.986667L213.333333 687.786667 102.4 450.56 781.653333 134.826667c20.48-10.24 44.373333 0 52.906667 18.773333l93.866667 201.386667z\" fill=\"#333333\" opacity=\".5\" ></path><path d=\"M459.093333 262.826667l-6.826666-51.2c-1.706667-15.36-13.653333-25.6-29.013334-25.6H114.346667c-15.36 0-29.013333 13.653333-29.013334 29.013333v658.773333c0 15.36 13.653333 29.013333 29.013334 29.013334h795.306666c15.36 0 29.013333-13.653333 29.013334-29.013334V317.44c0-15.36-13.653333-29.013333-29.013334-29.013333H488.106667c-15.36 0-27.306667-10.24-29.013334-25.6z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-guaduan\" viewBox=\"0 0 1024 1024\"><path d=\"M934.528 554.325333c-14.506667 54.229333-51.925333 69.674667-113.493333 60.970667-72.362667-10.24-133.589333-29.653333-129.109334-92.16 0 0 5.717333-67.285333-170.325333-72.405333-205.44-5.290667-196.693333 74.410667-196.693333 74.410666-0.085333 73.386667-50.773333 77.44-122.965334 88.192-72.234667 10.752-116.394667-7.168-125.269333-87.381333-17.152-154.624 265.386667-194.944 389.333333-196.48 0 0 189.013333-2.858667 279.594667 23.765333l48.768 13.866667c67.968 21.973333 128.981333 61.952 143.146667 118.826667 0 0 8.362667 25.898667-2.986667 68.394666z\" fill=\"#595959\" ></path></symbol></svg>',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(\"<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>\")}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);"
  },
  {
    "path": "src/assets/fonts/iconfont.json",
    "content": "{\n  \"id\": \"1698562\",\n  \"name\": \"vue-chat\",\n  \"font_family\": \"iconfont\",\n  \"css_prefix_text\": \"icon-\",\n  \"description\": \"\",\n  \"glyphs\": [\n    {\n      \"icon_id\": \"5100253\",\n      \"name\": \"视频\",\n      \"font_class\": \"shipin\",\n      \"unicode\": \"e669\",\n      \"unicode_decimal\": 58985\n    },\n    {\n      \"icon_id\": \"4425816\",\n      \"name\": \"删除-方框\",\n      \"font_class\": \"shanchu-fangkuang\",\n      \"unicode\": \"e791\",\n      \"unicode_decimal\": 59281\n    },\n    {\n      \"icon_id\": \"4770755\",\n      \"name\": \"增加数字\",\n      \"font_class\": \"zengjiashuzi\",\n      \"unicode\": \"e641\",\n      \"unicode_decimal\": 58945\n    },\n    {\n      \"icon_id\": \"6129045\",\n      \"name\": \"9减、删除-线性方框\",\n      \"font_class\": \"jianshanchu-xianxingfangkuang\",\n      \"unicode\": \"e75a\",\n      \"unicode_decimal\": 59226\n    },\n    {\n      \"icon_id\": \"8358855\",\n      \"name\": \"增加4\",\n      \"font_class\": \"zengjia2\",\n      \"unicode\": \"e689\",\n      \"unicode_decimal\": 59017\n    },\n    {\n      \"icon_id\": \"5253606\",\n      \"name\": \"移除\",\n      \"font_class\": \"yichu\",\n      \"unicode\": \"e656\",\n      \"unicode_decimal\": 58966\n    },\n    {\n      \"icon_id\": \"1301395\",\n      \"name\": \"增加\",\n      \"font_class\": \"zengjia\",\n      \"unicode\": \"e668\",\n      \"unicode_decimal\": 58984\n    },\n    {\n      \"icon_id\": \"1272671\",\n      \"name\": \"移除\",\n      \"font_class\": \"delete-br\",\n      \"unicode\": \"e63d\",\n      \"unicode_decimal\": 58941\n    },\n    {\n      \"icon_id\": \"4581221\",\n      \"name\": \"发送中\",\n      \"font_class\": \"loading-solid\",\n      \"unicode\": \"e647\",\n      \"unicode_decimal\": 58951\n    },\n    {\n      \"icon_id\": \"3398713\",\n      \"name\": \"发送失败\",\n      \"font_class\": \"fasongshibai\",\n      \"unicode\": \"e62c\",\n      \"unicode_decimal\": 58924\n    },\n    {\n      \"icon_id\": \"6659620\",\n      \"name\": \"朋友\",\n      \"font_class\": \"pengyou1\",\n      \"unicode\": \"e631\",\n      \"unicode_decimal\": 58929\n    },\n    {\n      \"icon_id\": \"3315144\",\n      \"name\": \"邀请好友\",\n      \"font_class\": \"yaoqinghaoyou\",\n      \"unicode\": \"e61c\",\n      \"unicode_decimal\": 58908\n    },\n    {\n      \"icon_id\": \"6683016\",\n      \"name\": \"加好友\",\n      \"font_class\": \"jiahaoyou\",\n      \"unicode\": \"e603\",\n      \"unicode_decimal\": 58883\n    },\n    {\n      \"icon_id\": \"795513\",\n      \"name\": \"视频\",\n      \"font_class\": \"ai-video\",\n      \"unicode\": \"e66b\",\n      \"unicode_decimal\": 58987\n    },\n    {\n      \"icon_id\": \"842937\",\n      \"name\": \"表情\",\n      \"font_class\": \"biaoqing\",\n      \"unicode\": \"e666\",\n      \"unicode_decimal\": 58982\n    },\n    {\n      \"icon_id\": \"2078817\",\n      \"name\": \"dkw_消息 \",\n      \"font_class\": \"dkw_xiaoxi\",\n      \"unicode\": \"e606\",\n      \"unicode_decimal\": 58886\n    },\n    {\n      \"icon_id\": \"2967254\",\n      \"name\": \"符号-电话\",\n      \"font_class\": \"dianhua\",\n      \"unicode\": \"e729\",\n      \"unicode_decimal\": 59177\n    },\n    {\n      \"icon_id\": \"4166140\",\n      \"name\": \"朋友\",\n      \"font_class\": \"pengyou\",\n      \"unicode\": \"e61a\",\n      \"unicode_decimal\": 58906\n    },\n    {\n      \"icon_id\": \"5582330\",\n      \"name\": \"搜索\",\n      \"font_class\": \"sousuo\",\n      \"unicode\": \"e659\",\n      \"unicode_decimal\": 58969\n    },\n    {\n      \"icon_id\": \"5893499\",\n      \"name\": \"退出\",\n      \"font_class\": \"tuichu\",\n      \"unicode\": \"e61f\",\n      \"unicode_decimal\": 58911\n    },\n    {\n      \"icon_id\": \"7588121\",\n      \"name\": \"图片\",\n      \"font_class\": \"tupian\",\n      \"unicode\": \"e623\",\n      \"unicode_decimal\": 58915\n    },\n    {\n      \"icon_id\": \"10099699\",\n      \"name\": \"文件\",\n      \"font_class\": \"wenjian\",\n      \"unicode\": \"e61b\",\n      \"unicode_decimal\": 58907\n    },\n    {\n      \"icon_id\": \"10515201\",\n      \"name\": \"挂断\",\n      \"font_class\": \"guaduan\",\n      \"unicode\": \"e640\",\n      \"unicode_decimal\": 58944\n    }\n  ]\n}\n"
  },
  {
    "path": "src/components/chatlist/chatlist.vue",
    "content": "<!-- 聊天列表 -->\n<template>\n  <div class=\"conversationlist\" :style=\"{height: (appHeight-60) + 'px'}\">\n    <ul v-loading=\"isEmptyConversation\" style=\"min-height: 60px\">\n        <li v-bind:key = index v-for=\"(item,index) in searchedConversationList\" class=\"sessionlist\" :class=\"{ active: item.conversationInfo.target === selectTarget }\" @click=\"selectConversation(item.conversationInfo.target)\">\n            <div class=\"list-left\">\n            \t<img class=\"avatar\"  width=\"42\" height=\"42\" alt=\"static/images/vue.jpg\" :src=\"item.img\" onerror=\"this.src='static/images/vue.jpg'\">\n            </div>\n            <div class=\"list-right\">\n                <div class=\"title-info\">\n                   <p class=\"name\">{{item.name ? item.name : \"\"}}</p>\n                   <span class=\"time\">{{item.conversationInfo.timestamp | getTimeStringAutoShort2}}</span>\n                </div>\n                <div class=\"lastmsg-info\">\n                    <p class=\"lastmsg\">{{processageGroupMessage(item)}}</p>\n                    <span v-if=\"item.conversationInfo.unreadCount && item.conversationInfo.unreadCount.unread > 0\" class=\"unread-num\">\n                        <span class=\"unread-num-show\">{{item.conversationInfo.unreadCount ? item.conversationInfo.unreadCount.unread : 0}}</span>\n                    </span>\n                </div>\n\n            </div>\n            \n        </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nimport { mapState, mapActions ,mapGetters } from 'vuex'\nimport ConversationType from '../../websocket/model/conversationType';\nimport LocalStore from '../../websocket/store/localstore';\nimport TimeUtils from '../../websocket/utils/timeUtils';\nimport MessageConfig from '../../websocket/message/messageConfig';\nimport NotificationMessageContent from '../../websocket/message/notification/notificationMessageContent';\nimport Logger from '../../websocket/utils/logger';\nimport webSocketClient from '../../websocket/websocketcli';\nexport default {\n    data(){\n        return {\n            loading: true\n        }\n    },\n    computed: {\n   \t    ...mapState([\n            'selectId',\n            'selectTarget',\n            'searchText',\n            'appHeight',\n            'userInfoList',\n            'emptyMessage'\n        ]),\n        ...mapGetters([\n            'searchedConversationList'\n        ]),\n        isEmptyConversation(){\n            return this.searchedConversationList.length == 0 && !this.emptyMessage;\n        }\n    },\n    methods: {\n    \t...mapActions([\n             'selectSession',\n             'selectConversation',\n        ]),\n        processageGroupMessage(item){\n            var protoConversationInfo = item.conversationInfo;\n            var displayContent;\n            if(protoConversationInfo.lastMessage){\n                var messageContent = MessageConfig.convert2MessageContent(protoConversationInfo.lastMessage.from,protoConversationInfo.lastMessage.content);\n                if(messageContent && messageContent instanceof NotificationMessageContent){\n                    displayContent = messageContent.formatNotification();\n                } else {\n                    displayContent = protoConversationInfo.lastMessage.content.searchableContent;\n                    if(protoConversationInfo.lastMessage.content.type === 400){\n                        displayContent = '[网络电话]';\n                    }\n                    var isCurrentUser = protoConversationInfo.lastMessage.from === LocalStore.getUserId();\n                    if(protoConversationInfo.conversationType == ConversationType.Group && !isCurrentUser){\n                        var from = protoConversationInfo.lastMessage.from;\n                        var displayName = this.getDisplayName(from);\n                        displayContent = displayName +\":\"+protoConversationInfo.lastMessage.content.searchableContent;\n                    }\n                }\n                \n            }\n           return displayContent;\n        },\n        getDisplayName(from){\n            var displayName = from;\n            displayName = webSocketClient.getDisplayName(from);\n            return displayName;\n        }\t\n    },\n    filters: {\n            // 将日期过滤为 hour:minutes\n            time (date) {\n                if (typeof date === 'string') {\n                    date = new Date(date);\n                }\n                if( typeof date === 'number'){\n                    date = new Date(date);\n                }\n                if(date.getMinutes()<10){\n                  return date.getHours() + ':0' +date.getMinutes();\n                }else{\n                  return date.getHours() + ':' + date.getMinutes();\n                }\n            },\n            getTimeStringAutoShort2(timestamp){\n                return TimeUtils.getTimeStringAutoShort2(timestamp,false);\n            }\n    },\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.conversationlist\n  height: 87%\n  overflow-y: auto\n  overflow-x: hidden\n  box-sizing: border-box\n  border-top: 1px solid #e7e7e7\n  border-right: 1px solid #e7e7e7\n  background: #f2f2f2\n  .sessionlist\n    display: flex\n    padding: 12px\n    transition: background-color .1s\n    font-size: 0\n    &:hover \n        background-color: rgb(220,220,220)\n    &.active \n        background-color: #c4c4c4\n    .avatar\n        border-radius: 2px\n        margin-right: 12px\n    .list-right\n        position: relative\n        flex: 1\n        margin-top: 4px\n        .title-info\n            display: flex\n            .name\n                flex: 1 1 auto\n                display: inline-block\n                width: 130px\n                height: 15px\n                line-height: 15px\n                font-size: 14px\n                overflow: hidden\n                white-space:nowrap\n                text-overflow:ellipsis\n            .time\n                flex: 0 0 auto\n                float: right\n                line-height: 15px\n                color: #999\n                font-size: 10px\n                vertical-align: center\n        .lastmsg-info\n            display: flex\n            margin-top: 8px    \n            .lastmsg\n                flex: 1 1 auto \n                font-size: 12px\n                width: 130px\n                height: 15px\n                line-height: 15px\n                color: #999\n                bottom: 4px\n                overflow: hidden\n                white-space:nowrap\n                text-overflow:ellipsis\n            .unread-num\n                flex: 0 0 auto\n                vertical-align:bottom\n                margin-top: 0px\n                display: inline-block;\n                min-width: 16px;\n                height: 16px;\n                background-color: red;\n                border-radius: 8px;\n                text-align: center;\n                font-size: 12px;\n                color: #fff;\n                line-height: 16px;\n                .unread-num-show\n                   text-align: center;\n                   font-size:10px;\n                   -webkit-transform:scale(0.8);\n                   display:block;\n          \n</style>\n"
  },
  {
    "path": "src/components/friendlist/friendlist.vue",
    "content": "<!-- 好友列表 -->\n<template>\n <div class=\"friendlist\" :style=\"{height: (appHeight-60) + 'px'}\">\n \t<ul>\n        <li v-bind:key = index v-for=\"(item, index) in searchedFriendlist\" class=\"frienditem\"  :class=\"{ noborder: !item.initial}\">\n            <div class=\"list_title\" v-if=\"item.initial\">{{item.initial}}</div>\n            <div class=\"friend-info\" :class=\"{ active: item.id === selectFriendId }\" @click=\"selectFriend(item.id)\">\n                <div class=\"friend-item\">\n                    <span v-if=\"newFriendRequestCount > 0 && item.id === 0\" class=\"unread-friend-request-num\">\n                            <span class=\"unread-num-show\">{{newFriendRequestCount}}</span>\n                    </span>\n                    <img class=\"avatar\"  width=\"36\" height=\"36\" :src=\"item.img\" onerror=\"this.src='static/images/vue.jpg'\">\n\t\t\t    </div>\n                <div class=\"remark\">{{item.remark}}</div>\n            </div>\n        </li>\n    </ul>\n </div>\n</template>\n\n<script>\nimport { mapState, mapActions ,mapGetters } from 'vuex'\nexport default {\n    computed: {\n        ...mapState([\n            'selectFriendId',\n            'searchText',\n            'appHeight',\n            'newFriendRequestCount'\n        ]),\n        ...mapGetters([\n            'searchedFriendlist',\n        ])\n    },\n    methods: {\n        ...mapActions([\n             'selectFriend',\n        ])  \n    }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.friendlist\n    height: 87%\n    overflow-y: auto\n    border-top: 1px solid #e7e7e7\n    border-right: 1px solid #e7e7e7\n    background: #f2f2f2\n    .frienditem\n        border-top: 1px solid #dadada\n        &:first-child,&.noborder\n            border-top: none\n        .list_title\n            box-sizing: border-box\n            width: 100%\n            font-size: 12px\n            padding: 15px 0 3px 12px\n            color: #999\n        .friend-info\n            display: flex\n            padding: 12px\n            transition: background-color .1s\n            font-size: 0\n            &:hover \n                background-color: rgb(220,220,220)\n            &.active \n                background-color: #c4c4c4\n            .avatar\n                border-radius: 2px\n                margin-right: 12px\n            .remark\n                font-size: 14px\n                line-height: 36px\n            .friend-item\n                position:relative\n                .unread-friend-request-num\n                    display: inline-block\n                    min-width: 16px\n                    height: 16px\n                    background-color: red\n                    border-radius: 8px\n                    text-align: center\n                    font-size: 12px\n                    color: #fff\n                    line-height: 16px\n                    position:absolute\n                    top: -5px\n                    right: 5px\n                    z-index: 10\n                    .unread-num-show\n                        font-size:10px;\n                        -webkit-transform:scale(0.8);\n                        display:block;\t\n\n\n</style>\n"
  },
  {
    "path": "src/components/info/info.vue",
    "content": "<!-- 好友信息 -->\n<template>\n   <div class=\"Info-wrapper\">\n        <div class=\"newfriend\">\n\t\t\t<div class=\"nickname\">{{selectedFriend.nickname}}</div>\n\t\t</div>\n\t\t<div class=\"friendrequest\" v-show=\"selectedFriend.id === 0\" :style=\"{height: (appHeight -60) + 'px'}\">\n           <el-table\n            :data=\"friendRequests\"\n            :show-header=\"false\"\n            :max-height=\"appHeight - 61\"\n            style=\"width: 100%;height:100%;\">\n            <el-table-column\n                prop=\"image\"\n                label=\"头像\"\n                width=\"50\">\n                <template slot-scope=\"scope\">\n                    <el-avatar :src=\"friendPortrait(scope.row.from)\"></el-avatar>\n                </template>\n            </el-table-column>\n            <el-table-column\n                prop=\"name\"\n                label=\"姓名\"\n                width=\"280\">\n                <template slot-scope=\"scope\">\n                    <div>\n                        <p>{{friendName(scope.row.from)}}</p>\n                        <p>{{scope.row.reason}}</p>\n                    </div>\n                </template>\n            </el-table-column>\n            <el-table-column\n                label=\"加好友\"\n                align=\"right\"\n                >\n                <template slot-scope=\"scope\">\n                    <!-- <el-button\n                        size=\"mini\"\n                        type=\"info\"\n                        @click=\"handleEdit(scope.$index, scope.row)\">拒绝</el-button> -->\n                    <el-button\n                        size=\"mini\"\n                        type=\"success\"\n                        :disabled=\"scope.row.status === 1\"\n                        @click=\"handleFriendRequest(scope.row)\">{{requestBtnMessage(scope.row)}}</el-button>\n                </template>\n            </el-table-column>\n           </el-table>\n\t\t</div>\n        <div class=\"friendInfo\" v-show=\"selectedFriend.id > 0\">\n\t   \t    <div class=\"esInfo\" >\n\t   \t    \t<div class=\"left\">\n\t   \t    \t\t<div class=\"people\">\n\t   \t    \t\t\t<div class=\"nickname\">{{selectedFriend.nickname}}</div>\n\t   \t    \t\t\t<div :class=\"[selectedFriend.sex===1?'gender-male':'gender-female']\"></div>\n\t   \t    \t\t</div>\n\t   \t    \t\t<div class=\"signature\">{{selectedFriend.signature}}</div>\n\t   \t    \t</div>\n\t   \t    \t<div class=\"right\">\n\t   \t    \t    <img class=\"avatar\"  width=\"60\" height=\"60\" :src=\"selectedFriend.img\">\n\t   \t    \t</div>\n\t   \t    </div>\n\t   \t    <div class=\"detInfo\">\n                <div class=\"remark\">\n                    <label class=\"title\" for=\"\">备注：</label>\n                    <p class=\"value\" contenteditable=\"true\" @keydown.enter=\"modifyRemark\" @blur=\"modifyRemarkBlur\">{{selectedFriend.remark}}</p>\n                </div>\n\t   \t    \t<div class=\"area\"><span>地&nbsp&nbsp&nbsp区</span>{{selectedFriend.area}}</div>\n\t   \t    \t<div class=\"wxid\"><span>微信号</span>{{selectedFriend.wxid}}</div>\n\t   \t    </div>\n\t   \t    <div class=\"send\" @click=\"send\">\n    \t        <span>发消息</span>\n            </div>\n\t   \t</div>\n   </div>\n</template>\n\n<script>\nimport router from '../../router'\nimport { mapGetters,mapState } from 'vuex'\nimport LocalStore from '../../websocket/store/localstore'\nimport webSocketCli from '../../websocket/websocketcli'\nimport { SUCCESS_CODE } from '../../constant'\nexport default {\n    computed: {\n        ...mapGetters([\n            'selectedFriend'\n        ]),\n        ...mapState([\n          'friendRequests',\n          'appHeight',\n          'userInfoList'\n        ]),\n        // friendRemark:{\n        //     get(){\n        //         console.log(\"remark test\")\n        //         return \"test\"\n        //         //return this.selectedFriend.remark \n        //     },\n        //     set(value){\n        //         console.log(\"remark vaule \"+value)\n        //         // this.selectedFriend.remark = value;\n        //     }\n        // }\n    },\n    methods: {\n    \tsend () {\n    \t\tthis.$store.dispatch('send')\n    \t\tthis.$store.dispatch('search', '')\n        },\n        requestBtnMessage(request){\n           if(request.status === 1){\n              return \"已添加\";\n           } else if(request.status === 2){\n              return \"已拒绝\"\n           } else if(request.status === 0){\n              return \"接受\"\n           }\n        },\n        handleFriendRequest(request){\n            this.$store.dispatch('handleFriendRequest', {\n                targetUid: request.from,\n                status: 1\n            })  \n        },\n        friendName(target){\n           var user = this.userInfoList.find(user => user.uid == target);\n           var displayName = target;\n           if(user){\n               displayName = user.displayName;\n               if(!displayName){\n                  displayName = user.mobile;\n               }\n           } \n           return displayName;\n        },\n        friendPortrait(target){\n            var user = this.userInfoList.find(user => user.uid == target);\n            var portrait;\n            if(user){\n                portrait = user.portrait;\n            }\n            \n            if(!portrait){\n                portrait =  \"static/images/vue.jpg\";\n            }\n            return portrait;\n        },\n        modifyRemark(e){\n            if(e.keyCode == 13){\n              e.preventDefault();\n              this.modifyRemarkBlur(e)\n           }\n        },\n        modifyRemarkBlur(e){\n            var remark = e.target.innerText;\n            if(remark == this.selectedFriend.remark){\n               return\n            }\n            this.selectedFriend.remark = remark;\n            if(remark != '' && remark.length < 15){\n                webSocketCli.modifyFriendAlias(this.selectedFriend.wxid,remark).then(data =>{\n                   if(data.code == SUCCESS_CODE){\n                       this.$message.success(\"修改好友备注成功\");\n                   } else {\n                       this.$message.error(\"修改好友备注失败\");\n                   }\n                })\n            } else {\n                this.$message.error(\"备注过长最好不要超过15个字符\");\n            }\n        }\n\t\t\n    }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.newfriend\n    height: 60px\n    padding: 28px 0 0 30px\n    box-sizing: border-box\n    .nickname\n        font-size: 18px\n.friendrequest\n    border-top: 1px solid #e7e7e7\n.friendInfo\n    padding: 0 90px\n    border-top: 1px solid #e7e7e7\n\t.esInfo\n\t    display: flex\n\t    align-items: center\n\t    padding: 100px 0 45px 0\n\t    .left\n\t        flex: 1\n\t        .people\n\t            .nickname\n\t                display: inline-block\n\t                font-size: 20px\n\t                margin-bottom: 16px\n\t            .gender-male,.gender-female\n\t                display: inline-block\n\t                width: 18px\n\t                height: 18px\n\t                vertical-align: top\n\t                margin-top: 2px\n\t            .gender-male\n\t                background-image: url(man.png)\n\t                background-size: cover\n\t            .gender-female\n\t                background-image: url(woman.png)\n\t                background-size: cover\n\t        .signature\n\t            font-size: 14px\n\t            color: rgba(153,153,153,.8)\n\t    .right\n\t        .avatar\n\t            border-radius: 3px\n\t.detInfo\n\t    padding: 40px 0\n\t    border-top: 1px solid #e7e7e7\n\t    border-bottom: 1px solid #e7e7e7\n\t    .remark,.area,.wxid\n\t        font-size: 14px\n\t        margin-top: 20px\n\t        span\n\t            font-size: 14px\n\t            color: rgba(153,153,153,.8)\n\t            margin-right: 40px\n\t    .remark\n            .title\n                float: left;\n                font-size: 14px;\n                line-height: 1.6\n                color: rgba(153,153,153,.8);\n                margin-right: 10px;\n            .value\n                font-size: 14px;\n                width: 200px;\n                line-height: 1.6\n                overflow: hidden;\n                text-overflow: ellipsis;\n                white-space: nowrap;\n                word-wrap: normal;\n                padding-left: 5px;\n                padding-right: 5px;    \n\t        margin-top: 0\n\t.send\n        position: relative\n        text-align: center\n        width: 140px\n        height: 36px\n        left: 115px\n        top: 50px\n        line-height: 36px\n        font-size: 14px\n        color: #fff\n        background-color: #1aad19\n        cursor: pointer\n        border-radius: 2px\n        &:hover\n            background: rgb(18,150,17)\n</style>\n\n"
  },
  {
    "path": "src/components/menu/addtip.vue",
    "content": "<template>\n <div class=\"add-content\">\n   <div>\n    <a @click=\"showSearchFriendDialog\" class=\"iconsize wx-chat-icon\"><i class=\"iconfont icon-jiahaoyou iconhover\"></i> 添加好友 </a>\n   </div> \n   <div>\n    <a @click=\"showCreateGroupDialog\" class=\"iconsize wx-chat-icon\"><i class=\"iconfont icon-pengyou iconhover\"></i> 创建群聊 </a>\n   </div>\n  </div>\n</template>\n\n<script>\nimport Logger from '../../websocket/utils/logger';\nexport default {\n\tname: 'addtip',\n\tmethods: {\n\t\tshowSearchFriendDialog(){\n            this.$store.state.showSearchFriendDialog = true;\n\t\t},\n\t\tshowCreateGroupDialog(){\n\t\t\tLogger.log(\"show create group dialog\");\n\t\t\tthis.$store.state.groupOperateState = 0;\n\t\t\tthis.$store.state.showCreateGroupDialog = true;\n\t\t}\n\t},\n}\n</script>\n\n<style scoped>\n.add-content {\n\tposition: absolute;\n\tbackground: #fff;\n\twidth: 112px;\n\ttext-align: center;\n\ttop: 200px;\n\tleft: 60px;\n\tz-index: 20;\n\tpadding: 8px 0;\n\tbox-shadow: 0 2px 6px 0 rgba(0,0,0,0.2);\n\tborder-radius: 4px\n}\n.add-content a {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\theight: 40px;\n\tposition: relative;\n\tcursor: pointer;\n\tfont-size: 14px;\n\tcolor: #333\n}\n\n.add-content a .iconfont {\n\tfont-size: 16px;\n\tcolor: #333;\n\tmargin-right: 8px\n}\n\n.add-content a:hover {\n\tcolor: rgb(0,220,65)\n}\n\n.add-content a:hover i {\n\tcolor: rgb(0,220,65)\n}\n\n.iconhover:hover {\n\tcolor: rgb(0,220,65)\n}\n\n.iconhover.on:hover {\n\tcolor: rgb(0,220,65)\n}\n</style>"
  },
  {
    "path": "src/components/menu/groupInfo.vue",
    "content": "<template>\n    <div id=\"group-info-id\" class=\"group-content\" :style=\"{height: (appHeight-61) + 'px'}\">\n        <div class=\"flex-layout\">\n            <div class=\"group-info-title\" v-if=\"!isSingleConversation\">\n                <div class=\"group-name-info\">\n                    <div class=\"group-name-title\">群名</div>\n                    <p class=\"group-name\" contenteditable=\"true\" @blur=\"modifyGroupNameBlur\" @keydown.enter=\"modifyGroupName\">{{groupName}}</p>\n                </div>\n                <div class=\"group-board-info\">\n                    <div class=\"group-board-title\">群公告</div>\n                    <div class=\"group-board-content\">暂无公告</div>\n                </div>\n            </div>\n            <div class=\"friend-info-list\" :style=\"{height: (appHeight-221) + 'px'}\">\n                <ul>\n                    <li v-bind:key = index v-for=\"(item, index) in memberList\" class=\"frienditem\">\n                        <div class=\"friend-info\">\n                            <i title=\"个人用户添加成员\" class=\"icon iconfont icon-zengjia add\" v-if=\"item.type == 1002\" @click=\"addGroupMemberFromSingle\"></i>\n                            <i title=\"添加成员\" class=\"icon iconfont icon-zengjia add\" v-if=\"item.type == 1000\" @click=\"addGroupMember\"></i>\n                            <i title=\"移除成员\" class=\"icon iconfont icon-shanchu-fangkuang add\" v-if=\"item.type == 1001\" @click=\"kickGroupMember\"></i>\n                            <img class=\"avatar\" :src=\"item.avatarUrl\" onerror=\"this.src='static/images/vue.jpg'\" v-if=\"item.type == 0 || item.type ==2\">\n                            <p class=\"nickName\">{{item.displayName}}</p>\n                        </div>\n                    </li>\n                </ul>\n            </div>\n            <div class=\"flex-bottom\" v-if=\"!isSingleConversation\">\n                <p class=\"quit-group\" v-if=\"!isGroupOwner\" @click=\"quitGroup\">退出群聊</p>\n                <p class=\"dismiss-group\" v-if=\"isGroupOwner\" @click=\"dismissGroup\">解散群聊</p>\n            </div>\n        </div>\n        \n    </div>\n\n</template>\n\n<script>\nimport { mapState, mapActions ,mapGetters } from 'vuex'\nimport LocalStore from '../../websocket/store/localstore';\n//注意websocketClient 不是常量引入\nimport webSocketClient from '../../websocket/websocketcli';\nimport { SUCCESS_CODE } from '../../constant';\nimport GroupMember from '../../websocket/model/groupMember';\nimport ModifyGroupInfoType from '../../websocket/message/modifyGroupInfoType'\nimport Logger from '../../websocket/utils/logger';\nexport default {\n    name: 'groupInfoMenu',\n    props: ['targetId'], \n    data(){\n        return {\n            isGroupOwner: false,\n            groupMembers: [],\n            addGroupMemberType: 1000,\n            deleteGroupMemberType: 1001,\n            addGroupMemberSingleType: 1002,\n        }\n    },\n    mounted() {\n        console.log(\"targetId \"+this.targetId)\n        document.addEventListener(\"click\", e => {\n            var isString = typeof(e.target.className) == 'string'\n            let groupInfoDom = document.getElementById(\"group-info-id\");\n            // console.log(\"isString \"+isString +\" show \"+e.target.className.search('show-group-info'))\n            //注意点击显示按钮事件的处理，防止状态发生反转\n\t\t\tif (isString && e.target.className.search('show-group-info') == -1 && groupInfoDom && !groupInfoDom.contains(e.target) && this.$store.state.showGroupInfo) {\n                this.$store.state.showGroupInfo = false;\n            }\n        });\n    },\n\n    destroyed() {\n       this.groupMembers = []\n    },\n    computed: {\n        ...mapState([\n            'appHeight',\n            'groupInfoList',\n            'userInfoList',\n            'groupMemberMap'\n        ]),\n        ...mapGetters([\n            'isSingleConversation'\n        ]),\n        \n        memberList(){\n            var groupMembers = [];\n            //add member item\n            \n            if(!this.isSingleConversation){\n                var addGroupMember = new GroupMember()\n                addGroupMember.type = this.addGroupMemberType;\n                addGroupMember.displayName = '添加成员';\n                groupMembers.push(addGroupMember);\n                //delete member\n                var deleteGroupMember = new GroupMember()\n                deleteGroupMember.type = this.deleteGroupMemberType;\n                deleteGroupMember.displayName = '移除成员';\n                groupMembers.push(deleteGroupMember);\n                if(this.groupMemberMap.get(this.targetId)){\n                    for(var groupMember of this.groupMemberMap.get(this.targetId)){\n                        if(groupMember.memberId == LocalStore.getUserId()){\n                                this.isGroupOwner = groupMember.type == 2 ? true : false;\n                        }\n                        groupMember.displayName = webSocketClient.getDisplayName(groupMember.memberId);\n                        groupMember.avatarUrl = webSocketClient.getPortrait(groupMember.memberId);\n                        groupMembers.push(groupMember);\n                    }\n                }\n                if(!this.isGroupOwner){\n                    groupMembers.splice(1,1);\n                }\n            } else {\n                var addGroupMember = new GroupMember()\n                addGroupMember.type = this.addGroupMemberSingleType;\n                addGroupMember.displayName = '添加成员';\n                groupMembers.push(addGroupMember);\n\n                var currentUser = new GroupMember();\n                currentUser.displayName = webSocketClient.getDisplayName(LocalStore.getUserId());\n                currentUser.avatarUrl = webSocketClient.getPortrait(LocalStore.getUserId());\n                currentUser.memberId = LocalStore.getUserId();  \n                groupMembers.push(currentUser)\n\n                var targetUser = new GroupMember();\n                targetUser.displayName = webSocketClient.getDisplayName(this.targetId);\n                targetUser.avatarUrl = webSocketClient.getPortrait(this.targetId);\n                targetUser.memberId = this.targetId;\n                groupMembers.push(targetUser);\n            }\n            \n            return groupMembers;\n        },\n\n        groupName: {\n            get() {\n                var groupName = \"\";\n                var groupInfo = this.groupInfoList.find(groupInfo => groupInfo.target == this.targetId);\n                if(groupInfo){\n                    groupName = groupInfo.name;\n                }\n                return groupName;\n            },\n            set(value){\n                var groupInfo = this.groupInfoList.find(groupInfo => groupInfo.target == this.targetId);\n                if(groupInfo){\n                    groupInfo.name = value;\n                }\n            }\n        }\n\n    },\n    methods: {\n        quitGroup(){\n            this.$confirm('你将退出此群组, 是否继续?', '提示', {\n                    confirmButtonText: '确定',\n                    cancelButtonText: '取消',\n                    type: 'warning'\n            }).then(() => {\n                webSocketClient.quitGroup(this.targetId).then(data => {\n                    if(data.code == SUCCESS_CODE){\n                        this.$message({\n                            type: 'success',\n                            message: '退出群组成功!'\n                        });\n                        this.$store.state.showGroupInfo = false;\n                        setTimeout(() => this.$store.dispatch('deleteConversation',this.targetId), 500) \n                    }\n               })  \n                    \n            }).catch(() => {\n                this.$message({\n                    type: 'info',\n                    message: '已取消退出群组'\n                });          \n            });  \n\n            \n        },\n        dismissGroup(){\n          this.$confirm('此操作将永久解散群组, 是否继续?', '提示', {\n                confirmButtonText: '确定',\n                cancelButtonText: '取消',\n                type: 'warning'\n            }).then(() => {\n                webSocketClient.dismissGroup(this.targetId).then(data => {\n                    if(data.code == SUCCESS_CODE){\n                        this.$message({\n                            type: 'success',\n                            message: '解散群组成功!'\n                        });\n                        this.$store.state.showGroupInfo = false;\n                        setTimeout(() => this.$store.dispatch('deleteConversation',this.targetId), 500)\n                    }\n                })\n                \n          }).catch(() => {\n            this.$message({\n                type: 'info',\n                message: '已取消解散群组'\n            });          \n          });    \n        },\n        addGroupMemberFromSingle(){\n            this.$store.state.groupOperateState = 3;\n            //触发groupMap以是vue相应变更\n            this.$store.state.groupMemberTracker += 1;\n            this.$store.state.showCreateGroupDialog = true;\n        },\n        addGroupMember(){\n            this.$store.state.groupOperateState = 1;\n            //触发groupMap以是vue相应变更\n            this.$store.state.groupMemberTracker += 1;\n            this.$store.state.showCreateGroupDialog = true;\n        },\n        kickGroupMember(){\n            this.$store.state.groupOperateState = 2;\n            //触发groupMap以是vue相应变更\n            this.$store.state.groupMemberTracker += 1;\n            this.$store.state.showCreateGroupDialog = true;\n        },\n        modifyGroupNameBlur(e){\n            var inputName = e.target.innerText;\n            if(this.groupName === inputName){\n                 return\n            }\n            this.groupName = inputName;\n            if(this.groupName && this.groupName.length < 15){\n                webSocketClient.modifyGroupInfo({\n                    groupId: this.targetId,\n                    type: ModifyGroupInfoType.Modify_Group_Name,\n                    value: this.groupName\n                });\n            } else {\n                this.$message.error(\"群组名称过长最好不要超过15个字符\");\n            }\n        },\n        modifyGroupName(e){\n           if(e.keyCode == 13){\n              this.modifyGroupNameBlur(e);\n              e.preventDefault();\n           }\n        }\n    },\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.group-content\n    position: absolute\n    background: #fff\n    height: 200px\n    width: 220px\n    top: 61px\n    right: 0px\n    z-index: 2000\n    border-left: 1px solid #e7e7e7 \n    .flex-layout\n        width: 100%\n        height: 100%\n        display: flex\n        flex-flow: row wrap\n        .group-info-title\n            margin-left: 10px\n            width: 100%\n            height: 120px\n            padding 10px 5px 10px 5px\n            border-bottom: 1px solid #e7e7e7 \n            .group-name-info\n                width: 100%\n                height: 50%\n                margin-bottom: 5px\n                .group-name-title\n                    font-size: 13px\n                    line-height: 1.6\n                    color: #888\n                .group-name\n                    font-size: 14px \n                    line-height: 1.6\n                    width: 200px  \n                    overflow: hidden\n                    white-space: nowrap\n                    text-overflow: ellipsis  \n            .group-board-info\n                width: 100%\n                height: 50%\n                .group-board-title \n                    font-size: 13px\n                    line-height: 1.6\n                    color: #888\n                .group-board-content \n                    font-size: 14px\n                    line-height: 1.6   \n        .friend-info-list\n            width: 100%\n            margin-left: 10px\n            overflow-y: auto\n            .frienditem\n                padding: 5px 0px 5px 0px\n            .friend-info\n                display: flex\n                padding: 5px\n                height: 40px\n                .avatar\n                    border-radius: 2px\n                    margin-right: 12px\n                    width: 32px\n                    height: 32px\n                .add\n                    margin-right: 12px\n                    font-size: 32px\n                    cursor: pointer\n                .nickName\n                    flex: 1 1 auto\n                    width: 106px\n                    font-size: 14px\n                    line-height: 32px\n                    overflow: hidden\n                    white-space: nowrap\n                    text-overflow: ellipsis\n                    color: black       \n        .flex-bottom\n            width: 100%\n            height: 40px\n            align-self: flex-end\n            display: flex\n            align-items: center\n            padding: 15px 0px 15px 0px\n            border-top: 1px solid #e7e7e7 \n            .quit-group\n                width: 100%\n                color: rgb(0,220,65);\n                font-size: 16px\n                cursor: pointer\n                text-align: center\n            .dismiss-group\n                width: 100%\n                color: red\n                font-size: 16px\n                cursor: pointer\n                text-align: center          \n</style>"
  },
  {
    "path": "src/components/menu/personalCard.vue",
    "content": "<template>\n    <div class=\"personal-card\" id=\"personal-card\">\n        <div class=\"profile_mini\">\n            <div class=\"profile_mini_hd\">\n                <div class=\"avatar\">\n                    <el-tooltip class=\"item\" effect=\"dark\" content=\"点击更换头像\" placement=\"top\">\n                        <div>\n                            <a href=\"javascript:void(0)\" id=\"add\" @click=\"openfile\">\n                                <img class=\"img\" :src=\"user.img\" alt=\"点击更换头像\">\n                            </a>\n                            <input type=\"file\" accept=\"image/*\" style=\"display:none\" ref=\"changeAvatar\" @change=\"changeAvatar\"/>\n                        </div>\n                    </el-tooltip>\n                </div>\n            </div>\n            <div class=\"profile_mini_bd\">\n                <div class=\"nickname_area\">\n                    <a class=\"opt\">\n                       <i class=\"web_wechat_tab_launch-chat\"></i>\n                    </a>\n                    <el-tooltip class=\"item\" effect=\"dark\" content=\"点击修改昵称，最长10个字符\" placement=\"top-start\">\n                        <div class=\"nickname_item\">\n                            <p class=\"nickname\" contenteditable=\"true\" @keydown.enter=\"modifyNickName\" @blur=\"modifyNickNameBlur\">{{displayName}}</p>\n                            <i  class=\"web_wechat_men\" v-show=\"false\"></i>\n                        </div>\n                    </el-tooltip>\n                </div>\n                <div class=\"meta_area\">\n                    <div class=\"meta_item\">\n                        <label class=\"label\" for=\"\">备注：</label>\n                        <p class=\"value\" contenteditable=\"true\" @keydown.enter=\"modifyExtra\">{{extra}}</p>\n                    </div>\n                    <div class=\"meta_item\">\n                        <label class=\"label\">手机号：</label>\n                        <p class=\"value\">{{mobile}}</p>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nimport Logger from '../../websocket/utils/logger';\nimport MyInfoType from '../../websocket/message/myInfoType'\nimport MessageContentMediaType from '../../websocket/message/messageContentMediaType';\nimport LocalStore from '../../websocket/store/localstore';\nimport * as qiniu from 'qiniu-js'\nimport { mapGetters, mapState } from 'vuex'\nimport { UPLOAD_BY_QINIU,SUCCESS_CODE } from '../../constant';\nimport webSocketCli from '../../websocket/websocketcli'\nexport default {\n    name: 'persionCard',\n    props: ['userId'],\n    data() {\n        return {\n            extra: '备注无',\n            address: '',\n        }\n    },\n    methods: {\n       modifyNickName(e){\n           if(e.keyCode ==13){\n              this.modifyNickNameBlur(e);\n              e.preventDefault();\n           }\n       },\n       modifyNickNameBlur(e){\n             Logger.log(\"modify displayName \"+e.target.innerText);\n              var inputName = e.target.innerText;\n              this.displayName = inputName;\n              if(inputName.length < 10){\n                 this.$store.dispatch(\"modifyMyInfo\",{\n                     type: MyInfoType.Modify_DisplayName,\n                     value: inputName\n                 });\n              }\n       },\n       modifyExtra(e){\n           if(e.keyCode ==13){\n              e.preventDefault();\n           }\n       },\n       openfile(){\n           this.$refs.changeAvatar.click();\n       },\n       changeAvatar(e){\n           var _this = this;\n           var store = this.$store;\n           if(UPLOAD_BY_QINIU){\n                store.dispatch('getUploadToken', MessageContentMediaType.Image);\n                console.log(\"changeAvatar \"+e.target.value);\n                var file = e.target.files[0];\n                var key = MessageContentMediaType.Image +\"-\"+LocalStore.getUserId()+\"-\"+new Date().getTime()+\"-\"+file.name;\n                setTimeout(()=> {\n                        var token = LocalStore.getImageUploadToken();\n                        console.log(\"upload avatar key \"+key+\" token \"+token);\n                        if(token){\n                            var observable = qiniu.upload(file, key, token, null, null);\n                            var observer = {\n                                    next(res){\n                                        console.log('uploading '+res.total.percent);\n                                    },\n                                    error(err){\n                                        console.log(\"upload error \"+err.code +\" message \"+err.message);\n                                    }, \n                                    complete(res){\n                                        console.log(\"upload complete \"+res);\n                                        var localPath = e.target.value;\n                                        var remotePath = \"http://image.comsince.cn/\"+key;\n                                        _this.$store.state.user.img = remotePath;\n                                        store.dispatch(\"modifyMyInfo\",{\n                                            type: MyInfoType.Modify_Portrait,\n                                            value: remotePath\n                                        });\n                                    }\n                                }\n                            observable.subscribe(observer);\n                        }\n                        \n                },200);\n           } else {\n               var file = e.target.files[0];\n               var key = MessageContentMediaType.Image +\"-\"+LocalStore.getUserId()+\"-\"+new Date().getTime()+\"-\"+file.name;\n               webSocketCli.getMinioUploadUrl(MessageContentMediaType.Image,key).then(data => {\n                    if(data.code == SUCCESS_CODE){\n                        console.log(\"domain \"+data.result.domain+\" url \"+data.result.url)\n                        fetch(data.result.url, {\n                            method: 'PUT',\n                            body: file\n                            }).then(() => {\n                                var remotePath = data.result.domain+\"/\"+key;\n                                _this.$store.state.user.img = remotePath;\n                                    store.dispatch(\"modifyMyInfo\",{\n                                            type: MyInfoType.Modify_Portrait,\n                                            value: remotePath\n                                });\n                            }).catch((e) => {\n                                console.error(e);\n                            });\n                    }\n                }) \n           }\n           \n           this.$refs.changeAvatar.value = null;\n       }\n    },\n    computed: {\n        ...mapState([   \n            'user',\n            'userInfoList'\n        ]),\n        personImg: {\n            get() {\n                var userInfo = this.userInfoList.find(user => user.uid == this.userId);\n                var portrait = '';\n                if(userInfo){\n                    portrait = userInfo.portrait;\n                } \n                if(portrait === ''){\n                    portrait = 'static/images/vue.jpg';\n                }\n                return portrait;\n            },\n            set(value) {\n                var userInfo = this.userInfoList.find(user => user.uid == this.userId);\n                if(userInfo){\n                    userInfo.portrait = value;\n                }\n            }\n\n        },\n        displayName: {\n           get() {\n                var userInfo = this.userInfoList.find(user => user.uid == this.userId);\n                var displayName = '';\n                if(userInfo){\n                    displayName = userInfo.displayName == '' ? userInfo.mobile : userInfo.displayName;\n                } \n                return displayName;\n           },\n           set(value) {\n                var userInfo = this.userInfoList.find(user => user.uid == this.userId);\n                if(userInfo){\n                    userInfo.displayName = value;\n                }\n           } \n        },\n        mobile() {\n            var userInfo = this.userInfoList.find(user => user.uid == this.userId);\n            if(userInfo){\n                 return userInfo.mobile;\n            }else {\n                return '';\n            }\n        }\n    }\n}\n</script>\n\n\n<style scoped>\n.personal-card {\n\tposition: absolute;\n\tbackground: #fff;\n\twidth: 250px;\n\ttop: 20px;\n\tleft: 60px;\n\tz-index: 20;\n\tbox-shadow: 0 2px 6px 0 rgba(0,0,0,0.2);\n\tborder-radius: 4px\n}\n\n.profile_mini_hd .avatar .img {\n    width: 250px;\n    height: 220px;\n    display: block;\n    border-top-left-radius:4px;\n    border-top-right-radius:4px;\n    cursor: pointer;\n}\n\n.profile_mini_bd {\n    padding: 20px;\n    min-height: 74px;\n}\n\n.profile_mini_bd .nickname_item {\n    overflow: hidden;\n}\n\n.profile_mini_bd .nickname_area {\n    margin-bottom: 8px;\n    line-height: 1.6;\n}\n\n.profile_mini_bd .opt {\n    float: right;\n}\n\n.web_wechat_tab_launch-chat {\n    display: inline-block;\n    vertical-align: middle;\n    width: 22px;\n    height: 22px;\n    background: url(https://res.wx.qq.com/a/wx_fed/webwx/res/static/css/5af37c4a880a95586cd41c5b251d5562@1x.png) no-repeat;\n    background-position: -223px -432px;\n    -webkit-background-size: 487px 462px;\n    background-size: 487px 462px;\n}\n\n.profile_mini_bd .nickname {\n    font-weight: 400;\n    font-size: 18px;\n    vertical-align: middle;\n    width: 115px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    word-wrap: normal;\n    line-height: 1.6;\n    margin-left: 5px;\n}\n\n.web_wechat_men {\n    display: inline-block;\n    vertical-align: middle;\n}\n\n.web_wechat_men {\n    width: 16px;\n    height: 16px;\n    background: url(https://res.wx.qq.com/a/wx_fed/webwx/res/static/css/5af37c4a880a95586cd41c5b251d5562@1x.png) no-repeat;\n    background-position: -384px -304px;\n    -webkit-background-size: 487px 462px;\n    background-size: 487px 462px;\n}\n\n.profile_mini_bd .meta_area {\n    line-height: 1.6;\n    margin-left: 5px;\n}\n\n.profile_mini_bd .meta_item {\n    overflow: hidden;\n}\n\n.profile_mini_bd .meta_item .label {\n    float: left;\n    font-size: 12px;\n    color: #888;\n    margin-right: 10px;\n}\n\n.profile_mini_bd .meta_item .value {\n    font-size: 12px;\n    color: #888;\n    width: 105px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    word-wrap: normal;\n    padding-left: 5px;\n    padding-right: 5px;\n}\n</style>"
  },
  {
    "path": "src/components/menu/relayMessage.vue",
    "content": "<template>\n    <div class=\"relay-message\">\n        <el-dialog\n        title=\"转发\"\n        :visible.sync=\"showRelayMessageDialog\"\n        width=\"30%\"\n        :show-close=\"true\"\n        @close=\"handleClose\"\n        center>\n\n        <el-dialog\n          width=\"30%\"\n          title=\"转发\"\n          :visible.sync=\"innerVisible\"\n          append-to-body>\n          <div v-if=\"isTextMessage\">\n            <el-input  \n             type=\"textarea\"\n             :rows=\"3\"\n             v-model=\"waitRelayMessage\"></el-input>\n          </div>\n\n          <div v-if=\"isImageMessage\" style=\"text-align:center\">\n                <img :src=\"waitRelayImageUrl\" class=\"preview-image\">\n          </div>\n\n          <div v-if=\"isVideoMessage && innerVisible\" style=\"text-align:center\">\n            <Xgplayer :config=\"videoConfig\" @player=\"Player = $event\"/>\n          </div>\n          \n          <span slot=\"footer\" class=\"dialog-footer\">\n            <el-button size=\"medium\" type=\"info\" plain round @click=\"innerVisible=false\">取 消</el-button>\n            <el-button size=\"medium\" type=\"success\" plain round @click=\"sendRelayMessage\">确 定</el-button>\n        </span>   \n        </el-dialog>\n\n        <el-input v-model=\"conversationInput\" prefix-icon=\"el-icon-search\" placeholder=\"请输入会话名称\" @keydown.enter.native=\"searchConversation\"></el-input>\n        <el-table\n            :data=\"conversationList\"\n            :show-header=\"false\"\n            :fit=\"true\"\n            :highlight-current-row=\"false\"\n            max-height=\"300\"\n            @row-click=\"showRelayMessageRequestDialog\"\n            style=\"width: 100%;margin-top:10px\">\n            <el-table-column\n                prop=\"image\"\n                label=\"会话头像\"\n                width=\"50\"\n                >\n                <template slot-scope=\"scope\">\n                    <el-avatar :src=\"scope.row.image\"></el-avatar>\n                </template>\n            </el-table-column>\n            <el-table-column\n                prop=\"name\"\n                label=\"会话名称\">\n                <template slot-scope=\"scope\">\n                    <div>\n                        <p class=\"conversation-name\">{{scope.row.name}}</p>\n                    </div>\n                </template>\n            </el-table-column>\n        </el-table>\n        </el-dialog>\n    </div>\n</template>\n\n<script>\nimport { mapGetters, mapState } from 'vuex'\nimport MessageConfig from '../../websocket/message/messageConfig';\nimport TextMessageContent from '../../websocket/message/textMessageContent';\nimport ImageMessageContent from '../../websocket/message/imageMessageContent';\nimport SendMessage from '../../websocket/message/sendMessage'\nimport Xgplayer from 'xgplayer-vue';\nimport VideoMessageContent from '../../websocket/message/videoMessageContent';\nexport default {\n    components:{\n        Xgplayer\n    },\n    data() {\n        return {\n            conversationInput: '',\n            waitRelayMessage: '',\n            waitRelayImageUrl: '',\n            innerVisible: false,\n            waitSendTarget: null,\n            currentSendMessage: null,\n            videoConfig: null,\n            Player: null,\n        }\n    },\n    methods: {\n      changeRelayMessageDialog(){\n        this.$store.state.showRelayMessageDialog = false;\n      },\n      searchConversation(e){\n         console.log(\"search conversation \");\n         if(e.keyCode === 13 && this.conversationInput != \"\"){\n            //this.$store.dispatch('searchUser', this.conversationInput);\n            this.conversationInput = '';\n         }\n      },\n      handleClose(){\n          //this.$store.dispatch('updateSearchUser', []);\n      },\n      showRelayMessageRequestDialog(row, event, column){\n          var target = row.target;\n          this.innerVisible=true;\n          if(this.currentRightMenuMessage){\n              console.log(\"conent type \"+this.currentRightMenuMessage.content.type)\n              var messageContent = MessageConfig.convert2MessageContent(this.currentRightMenuMessage.from,this.currentRightMenuMessage.content)\n              if(messageContent instanceof TextMessageContent){\n                   this.waitRelayMessage = messageContent.digest();\n              } else if(messageContent instanceof ImageMessageContent){\n                   this.waitRelayImageUrl = messageContent.remotePath;\n              } else if(messageContent instanceof VideoMessageContent){\n                  var posterBase64 = \"data:image/png;base64,\"+messageContent.thumbnail;\n                   this.videoConfig = {\n                        id: 'vs'+new Date().getTime(),\n                        // url 为空,可能导致不显示,这里强制写入poster\n                        url: messageContent.remotePath == ''? posterBase64: messageContent.remotePath,\n                        // height: 330,\n                        // width: 250,\n                        fluid: true,\n                        // fitVideoSize: 'fixHeight',\n                        poster: posterBase64,\n                        autoplay: false,\n                        download: true\n                    }\n              }\n              this.currentSendMessage = new SendMessage(target,messageContent);\n          }\n      },\n      sendRelayMessage(){\n        this.innerVisible=false;\n        this.$store.dispatch('sendMessage', this.currentSendMessage)\n      },\n      \n   },\n   computed: {\n     ...mapState([\n         'currentRightMenuMessage'\n        ]),\n    ...mapGetters([\n            'searchedConversationList',\n        ]),    \n     showRelayMessageDialog : {\n         get () {\n             return this.$store.state.showRelayMessageDialog;\n         },\n         set(val) {\n             this.$store.state.showRelayMessageDialog = val;\n         }\n     },\n     conversationList(){\n         var conversationList = [];\n         if(this.searchedConversationList){\n             for(var conversationInfo of this.searchedConversationList){\n                conversationList.push({\n                    image : conversationInfo.img,\n                    name : conversationInfo.name,\n                    target : conversationInfo.conversationInfo.target\n                })\n             }\n         }\n         return conversationList;\n     },\n     isTextMessage(){\n          var flag = false\n          if(this.currentRightMenuMessage){\n              flag = this.currentRightMenuMessage.content.type == 1\n          }\n          console.log(\"flag \"+flag)\n          return flag\n     },\n     isImageMessage(){\n         var flag = false\n          if(this.currentRightMenuMessage){\n              flag = this.currentRightMenuMessage.content.type == 3\n          }\n          console.log(\"flag \"+flag)\n          return flag\n     },\n     isVideoMessage(){\n         var flag = false\n          if(this.currentRightMenuMessage){\n              flag = this.currentRightMenuMessage.content.type == 6\n          }\n          console.log(\"flag \"+flag)\n          return flag\n     }\n   }\n}\n</script>\n\n<style scoped>\n\n.preview-image{\n    max-width : 115px;\n    max-height : 330px;\n    text-align: center;\n    border-radius: 3px\n}\n      \n\n.relay-message .icon {\n   display: inline-block;\n   font-size: 26px; \n   color: rgb(0,220,65);\n}\n\n.conversation-name {\n    line-height: 15px;\n    font-size: 14px;\n    width: 300px;\n    overflow: hidden;\n    white-space:nowrap;\n    text-overflow:ellipsis;\n}\n\n.layout-container {\n    width: 100%;\n    height: 100%;\n    box-sizing: border-box;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column\n}\n\n.el-dialog__wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n</style>"
  },
  {
    "path": "src/components/menu/rightMenu.vue",
    "content": "<template>\n  <div v-bind:class=\"menuStyle\" @contextmenu.prevent=\"\">\n   <div @click=\"recallMessage\" v-if=\"isFrom\">\n    <a> 撤回消息 </a>\n   </div> \n   <div @click=\"deleteMessage\">\n    <a> 删除消息 </a>\n   </div>\n   <div @click=\"relayMessage\">\n    <a> 转发 </a>\n   </div>\n  </div>\n</template>\n\n<script>\nimport { mapGetters, mapState } from 'vuex'\nimport webSocketClient from '../../websocket/websocketcli';\nimport { SUCCESS_CODE } from '../../constant';\nimport RecallMessageNotification from '../../websocket/message/notification/recallMessageNotification';\nimport LocalStore from '../../websocket/store/localstore';\nexport default {\n\tname: 'rightmenu',\n\tprops:['message'],\n\tcomputed:{\n\t\t...mapState([\n\t\t\t'showMessageRightMenu',\n\t\t]),\n\t\tmenuStyle(){\n            return {\n\t\t\t\t\"right-menu-content\": this.message.direction == 0,\n\t\t\t\t\"right-menu-content-left\": this.message.direction != 0\n\t\t\t}\n\t\t},\n\t\tisFrom(){\n\t\t\t// return this.message.direction == 0\n\t\t\treturn true\n\t\t}\n\t},\n\tmounted() {\n        document.addEventListener(\"click\", e => {\n            var isString = typeof(e.target.className) == 'string'\n            let rightMenuDom = document.getElementById(\"right-menu-content\");\n\t\t\t//注意点击显示按钮事件的处理，防止状态发生反转\n\t\t\tvar menuSetting = this.showMessageRightMenu.find(setting => setting.messageId == this.message.messageId)\n\t\t\tvar show = menuSetting ? menuSetting.show : false\n\t\t\t// console.log(\"isString \"+isString +\" show \" +e.target.className.search('right-menu-content')+\" show \"+show+\" contain \"+rightMenuDom)\n\t\t\tif (isString && e.target.className.search('right-menu-content') == -1 && show) {\n\t\t\t\tthis.noShowMenu();\n            }\n        });\n    },\n\tmethods:{\n\t\trecallMessage(){\n\t\t\tthis.$store.state.currentRightMenuMessage = this.message\n\t\t\twebSocketClient.recallMessage(this.message.messageUid).then(data => {\n\t\t\t\tif(data.code == SUCCESS_CODE){\n\t\t\t\t\tif(data.result == 200){\n\t\t\t\t\t\tvar recallMessageContent = new RecallMessageNotification(LocalStore.getUserId(),this.message.messageUid);\n\t\t\t\t\t\tthis.message.content = recallMessageContent.encode();\n\t\t\t\t\t\tconsole.log(\"recall message \"+this.message.messageId);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.$message.error('非管理员只能撤回自己发送的消息');\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t});\n\t\t\tthis.noShowMenu();\n\t\t},\n\t\tdeleteMessage(){\n\t\t\tthis.$store.state.currentRightMenuMessage = this.message\n\t\t\tconsole.log(\"delete message id \"+this.message.messageId)\n\t\t\tthis.$store.dispatch('deleteMessage',this.message.messageId)\n            this.noShowMenu();\n\t\t},\n\t\trelayMessage(){\n\t\t\tthis.$store.state.currentRightMenuMessage = this.message\n\t\t\tthis.$store.state.showRelayMessageDialog = true;\n            this.noShowMenu();\n\t\t},\n\t\tnoShowMenu(){\n\t\t\tvar menuSetting = this.showMessageRightMenu.find(setting => setting.messageId == this.message.messageId)\n\t\t\tif(menuSetting){\n                menuSetting.show = false;\n\t\t\t}\n\t\t}\n\t}\n}\n</script>\n\n\n<style scoped>\ndiv {\n\t-o-user-select: none;\n    -moz-user-select: none; /*火狐 firefox*/\n    -webkit-user-select: none; /*webkit浏览器*/\n    -ms-user-select: none; /*IE10+*/\n    -khtml-user-select :none; /*早期的浏览器*/\n    user-select: none; \n}\n.right-menu-content {\n\tposition: absolute;\n\tbackground: #fff;\n\twidth: 80px;\n\ttext-align: center;\n\tright: 0px;\n\tbottom: 40px;\n\tz-index: 2000;\n\tpadding: 2px 0;\n\tbox-shadow: 0 2px 6px 0 rgba(0,0,0,0.2);\n\tborder-radius: 4px\n}\n\n.right-menu-content-left {\n\tposition: absolute;\n\tbackground: #fff;\n\twidth: 80px;\n\ttext-align: center;\n\tleft: 0px;\n\tbottom: 40px;\n\tz-index: 2000;\n\tpadding: 2px 0;\n\tbox-shadow: 0 2px 6px 0 rgba(0,0,0,0.2);\n\tborder-radius: 4px\n}\n\ndiv a {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\theight: 30px;\n\tposition: relative;\n\tcursor: pointer;\n\tfont-size: 12px;\n\tcolor: #333\n}\n\ndiv a:hover {\n\tcolor: rgb(0,220,65)\n}\n\ndiv a:hover i {\n\tcolor: rgb(0,220,65)\n}\n\n.iconhover:hover {\n\tcolor: rgb(0,220,65)\n}\n\n.iconhover.on:hover {\n\tcolor: rgb(0,220,65)\n}\n</style>"
  },
  {
    "path": "src/components/message/message.vue",
    "content": "<!-- 消息框 -->\n<template>\n\t<div class=\"message\">\n\t\t<header class=\"header\">\n\t\t\t<div class=\"friendname\">{{selectedChat.name}}</div>\n            <div class=\"friend-group-info\">\n                <i title=\"用户信息\" class=\"icon iconfont icon-pengyou1 show-group-info\" v-if=\"isSingleConversation\" @click=\"changeShowCreateGroup\"></i>\n                <i title=\"群组信息\" class=\"icon iconfont icon-pengyou show-group-info\" v-if=\"!isSingleConversation\" @click=\"changeShowGroupInfo\"></i>\n                <groupInfo v-bind:targetId=\"selectedChat.target\" v-if=\"showGroupInfo\"></groupInfo>\n            </div>\n\t\t</header>\n\t\t<div class=\"message-wrapper\" ref=\"list\" @scroll=\"scrollEvent\" @click=\"messageBoxClick\" :style=\"{height: (appHeight * 0.75-60) + 'px'}\">\n\t\t    <div v-if=\"isLoading\" class=\"loading\"><img src=\"../../assets/img/loading.gif\" />{{loadingDes}}</div>\n            <ul v-if=\"selectedChat\">\n\t\t    \t<li v-bind:key = index v-for=\"(item, index) in selectedChat.protoMessages\" class=\"message-item\">\n\t\t    \t\t<div v-if=\"isShowTime(index,selectedChat.protoMessages)\" class=\"time\"><span>{{item.timestamp | getTimeStringAutoShort2}}</span></div>\n                    <div v-if=\"isGroupNotification(item)\" class=\"time\"><span>{{notificationContent(item)}}</span></div>\n                    <div v-if=\"isRecallNotification(item)\" class=\"time\"><span>{{notificationContent(item)}}</span></div>\n                    <div v-if=\"item.content.type === 90\" class=\"time\"><span>{{item.content.content}}</span></div>\n\t\t    \t\t<div v-if=\"!isNotification(item.content.type)\" class=\"main\" :class=\"{ self: item.direction == 0 ? true : false }\">\n                        <img class=\"avatar\" width=\"36\" height=\"36\" :src=\"avatarSrc(item)\"\n                        onerror=\"this.src='static/images/vue.jpg'\"/>\n                        <div class=\"content\">\n                            <div class=\"display-name\" v-if=\"!isSingleConversation && item.direction != 0\">{{showUserName(item.from)}}</div>\n\n                            <div class=\"content-message-right-menu\">\n                                <div class=\"send-status\" v-if=\"item.direction == 0\">\n                                    <i title = \"发送中\" class=\"icon iconfont icon-loading-solid\" v-if=\"isSending(item)\"></i>\n                                    <i title = \"发送失败\" class=\"icon iconfont icon-fasongshibai\" v-if=\"isSendFail(item)\"></i>\n                                </div>\n                                <div class=\"content-message\" @contextmenu.prevent=\"messageRigthClick(item.messageId)\">\n                                    <div v-if=\"item.content.type === 1 && isfaceMessage(item.content.searchableContent)\" class=\"text\" v-html=\"replaceFace(item.content.searchableContent)\"></div>\n                                    <div v-if=\"item.content.type === 1 && !isfaceMessage(item.content.searchableContent)\" class=\"text\" v-text=\"item.content.searchableContent\"></div>    \n                                    <div v-if=\"item.content.type === 2\">\n                                        [语音消息]\n                                    </div>\n                                    <div v-if=\"item.content.type === 3\" v-viewer=\"options\">\n                                        <img :src=\"imageThumnailSrc(item)\" :data-src=\"item.content.remoteMediaUrl\" class=\"receive-image\">\n                                    </div>\n                                    <div v-if=\"item.content.type === 4\">\n                                        [位置消息]\n                                    </div>\n                                    <div v-if=\"item.content.type === 5\">\n                                        <div class=\"attachment\"> \n                                            <div class=\"flexbox flex-alignc\"> \n                                                <i class=\"ico-bg\"></i> \n                                                    <div class=\"file-info flex1\"> \n                                                        <p class=\"name\">{{fileMessageConfig(item).name}}</p>\n                                                        <p class=\"size\">{{fileMessageConfig(item).size}}</p>\n                                                    </div> \n                                                <a class=\"btn-down\" :href=\"fileMessageConfig(item).remotePath\" target=\"_blank\"></a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                    <div v-if=\"item.content.type === 6\" >\n                                        <Xgplayer :config=\"videoConfig(item,false,imageThumnailSrc(item))\" @player=\"Player = $event\"/>\n                                    </div>\n                                    <div v-if=\"item.content.type === 7\">\n                                        [表情消息]\n                                    </div>\n                                    <div v-if=\"item.content.type === 8\">\n                                        [图片表情]\n                                    </div>\n                                    <div v-if=\"item.content.type === 400\">\n                                        [网络电话]\n                                    </div>\n                                </div>\n                                <rightMenu v-if=\"isShowMessageMenu(item)\" v-bind:message=\"item\"></rightMenu>\n                            </div>\n                            \n                        </div>\n                        \n                    </div>\n\t\t    \t</li>\n\t\t    </ul>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { mapGetters,mapActions, mapState } from 'vuex'\nimport TimeUtils from '../../websocket/utils/timeUtils'\nimport Xgplayer from 'xgplayer-vue';\nimport 'viewerjs/dist/viewer.css'\nimport Viewer from 'v-viewer'\nimport Vue from 'vue'\nVue.use(Viewer)\nimport CryptoJS from 'crypto-js'\nimport groupInfo from '../menu/groupInfo'\nimport rightMenu from '../menu/rightMenu'\nimport MessageConfig from '../../websocket/message/messageConfig';\nimport NotificationMessageContent from '../../websocket/message/notification/notificationMessageContent';\nimport GroupNotificationContent from '../../websocket/message/notification/groupNotification';\nimport MessageStatus from '../../websocket/message/messageStatus';\nimport RecallMessageNotification from '../../websocket/message/notification/recallMessageNotification';\nimport webSocketClient from '../../websocket/websocketcli';\nimport { SUCCESS_CODE } from '../../constant';\nimport LocalStore from '../../websocket/store/localstore';\nimport Conversation from '../../websocket/model/conversation';\nimport ConversationType from '../../websocket/model/conversationType';\nimport ProtoMessage from '../../websocket/message/protomessage'\nexport default {\n    components:{\n        Xgplayer,\n        groupInfo,\n        rightMenu\n    },\n\n    data(){\n        return {\n            Player: null,\n            options: {\n                url: 'data-src'\n            },\n            messageDatalistHeight:0,\n            isLoading: false,\n            loadingDes: '数据载入中...'\n        }\n    },\n    \n    computed: {\n        ...mapGetters([\n            'selectedChat',\n            'messages',\n            'isSingleConversation'\n        ]),\n        ...mapState([\n            'user',\n            'emojis',\n            'appHeight',\n            'userInfoList',\n            'showGroupInfo',\n            'showMessageRightMenu',\n            'groupMemberMap',\n            'isLoadRemoteMessage'\n        ]),\n        showGroupInfo: {\n           get() {\n               return this.$store.state.showGroupInfo;\n           },\n\n           set(value){\n                this.$store.state.showGroupInfo = value\n           }\n        },\n        \n    },\n    mounted() {\n         //  在页面加载时让信息滚动到最下面\n        setTimeout(() => this.$refs.list.scrollTop = this.$refs.list.scrollHeight, 0);\n        // document.addEventListener(\"click\", e => {\n        //     var isString = typeof(e.target.className) == 'string'\n        //     let groupInfoDom = document.getElementById(\"group-info-id\");\n        //     //注意点击显示按钮事件的处理，防止状态发生反转\n\t\t// \tif (isString && e.target.className.search('show-group-info') == -1 && groupInfoDom && !groupInfoDom.contains(event.target) && this.showGroupFriendInfo) {\n        //         this.showGroupFriendInfo = false;\n        //     }\n        // });\n        \n    },\n    watch: {\n        // 发送信息后,让信息滚动到最下面\n        messages() {\n          setTimeout(() => {\n              if(!this.isLoadRemoteMessage){\n                this.$refs.list.scrollTop = this.$refs.list.scrollHeight\n              } else {\n                  var currentListheight = this.$refs.list.scrollHeight;\n                  console.log(\"currentListheight \"+currentListheight+\" messageDatalistHeight \"+this.messageDatalistHeight)\n                  this.$refs.list.scrollTop = currentListheight - this.messageDatalistHeight\n                  this.messageDatalistHeight = currentListheight\n              }\n          }, 0)\n        }\n    },\n    methods: {\n        ...mapActions([\n             'addOldMessage',\n        ]),\n        changeShowGroupInfo(){\n            if(this.showGroupInfo){\n                this.showGroupInfo = false\n                return\n            }\n            webSocketClient.getGroupMember(this.selectedChat.target).then(data => {\n                var isGroupMember = false;\n\n                if(data.code == SUCCESS_CODE){\n\n                    if(data.result.length == 0){\n                        this.$message.error(\"该群组已经解散,即将删除该会话\");\n                        this.$store.dispatch('deleteConversation',this.selectedChat.target)\n                    } else {\n                        this.groupMemberMap.set(this.selectedChat.target,data.result);\n\n                        for(var groupMember of data.result){\n                            if(groupMember.memberId == LocalStore.getUserId()){\n                                isGroupMember = true;\n                                break;\n                            }\n                            \n                        }\n\n                        if(!isGroupMember){\n                            this.$message.error(\"您不是群组成员，无法查看群组信息,即将删除该会话\");\n                            this.$store.dispatch('deleteConversation',this.selectedChat.target)\n                        } else {\n                            this.showGroupInfo = !this.showGroupInfo ;\n                            if(this.showGroupInfo){\n                                this.$store.dispatch(\"getGroupInfo\",this.selectedChat.target);\n                            }\n                        }\n                    }\n                    \n                }\n            })\n        },\n        changeShowCreateGroup(){\n            this.showGroupInfo = !this.showGroupInfo;\n        },\n        avatarSrc(item){\n            var avarimgUrl = 'static/images/vue.jpg';\n            if(item.direction == 0){\n                avarimgUrl = this.user.img;\n            } else {\n                var user = this.userInfoList.find(user => user.uid == item.from);\n                if(user){\n                   avarimgUrl = user.portrait;\n                }\n            }\n            return avarimgUrl;\n        },\n        showUserName(from){\n            var displayName = webSocketClient.getDisplayName(from);;\n            return displayName;\n        },\n        //  在发送信息之后，将输入的内容中属于表情的部分替换成emoji图片标签\n        //  再经过v-html 渲染成真正的图片\n        replaceFace (con) {\n            if(con.includes('/:')) {\n                var emojis=this.emojis;\n                for(var i=0;i<emojis.length;i++){\n                    con = con.replace(emojis[i].reg, '<img src=\"static/emoji/' + emojis[i].file +'\"  alt=\"\" style=\"vertical-align: middle; width: 24px; height: 24px\" />');\n                }   \n                return con;\n            }\n            return con;\n        },\n\n        isfaceMessage(con){\n            return con.includes('/:');\n        },\n\n        isShowTime(index,protoMessages){\n           var msgTime = protoMessages[index].timestamp;\n           if(index > 0){\n               var preProtoMessage = protoMessages[index - 1];\n               var preMsgTime = preProtoMessage.timestamp;\n               if(msgTime - preMsgTime > ( 5 * 60 * 1000)){\n                   return true;\n               }\n           }\n           return false;\n        },\n\n        videoConfig(protoMessage,paly = false,posterBase64){\n           return {\n            id: 'vs'+protoMessage.messageId,\n            // url 为空,可能导致不显示,这里强制写入poster\n            url: protoMessage.content.remoteMediaUrl == ''? posterBase64: protoMessage.content.remoteMediaUrl,\n            height: 330,\n            width: 250,\n            // fitVideoSize: 'auto',\n            poster:posterBase64,\n            autoplay: false,\n            download: true\n           }\n        },\n        // 参考资料 https://blog.csdn.net/qq449736038/article/details/80769507\n        scrollEvent(e){\n            let listheight= this.$refs.list.offsetHeight;\n            //console.log('scroll event top->'+e.srcElement.scrollTop+ ' scrollheight '+e.srcElement.scrollHeight+\" list height->\"+listheight);\n             if(e.srcElement.scrollHeight - e.srcElement.scrollTop > listheight){\n                 this.$store.dispatch('clearUnreadStatus', '')\n             }\n             if(e.srcElement.scrollTop == 0){\n                 this.isLoading = true\n                 this.loadingDes = \"数据载入中...\"\n                 this.messageDatalistHeight = e.srcElement.scrollHeight;\n                 console.log(\"scroll height \"+listheight)\n                 var conversationType = this.isSingleConversation ? ConversationType.Single : ConversationType.Group\n                 var conversation = new Conversation(conversationType,this.selectedChat.target,0)\n                 var beforeUid = 0;\n                 if(this.selectedChat.protoMessages && this.selectedChat.protoMessages.length > 0){\n                     beforeUid = this.selectedChat.protoMessages[0].messageId\n                     console.log(\"beforeUid \"+beforeUid)\n                 }\n                 webSocketClient.getRemoteMessages(conversation,beforeUid,20).then(data => {\n                     this.isLoading = false;\n                     //console.log('code '+data.code+' result '+data.result)\n                     if(data.result){\n                        var remoteMessage = JSON.parse(data.result)\n                        var count = remoteMessage.count\n                        console.log(\"message count \"+count)\n                        if(count > 0){\n                           var messageList = remoteMessage.messageResponseList;\n                           for(var originProtoMessage of messageList ){\n                               var protoMessage = ProtoMessage.toProtoMessage(originProtoMessage);\n                               this.addOldMessage(protoMessage)\n                           }\n                        }\n                     }\n                 })\n             }\n        },\n\n        messageBoxClick(e){\n            this.$store.dispatch('clearUnreadStatus', '')\n            console.log('message box click');\n        },\n\n        isGroupNotification(protoMessage){\n           var contentClass = MessageConfig.getMessageContentClazz(protoMessage.content.type);\n           var content = new contentClass();\n           return content instanceof GroupNotificationContent;\n        },\n\n        isRecallNotification(protoMessage){\n           var contentClass = MessageConfig.getMessageContentClazz(protoMessage.content.type);\n           var content = new contentClass();\n           return content instanceof RecallMessageNotification;\n        },\n\n        notificationContent(protoMessage){\n            var displayContent;\n            var messageContent = MessageConfig.convert2MessageContent(protoMessage.from,protoMessage.content);\n            return messageContent.formatNotification();\n        },\n\n        fileMessageConfig(protoMessage){\n           var fileMessageContent = MessageConfig.convert2MessageContent(protoMessage.from,protoMessage.content)\n           return fileMessageContent;\n        },\n\n        isNotification(type){\n            return type >= 80 && type <= 117 \n        },\n\n        isSending(protoMessage){\n            //兼容以前没有更新messageUid的发送消息\n            return protoMessage.status == MessageStatus.Sending\n            // && protoMessage.messageUid != 0;\n        },\n\n        isSendFail(protoMessage){\n            return protoMessage.status == MessageStatus.SendFailure\n        },\n\n        messageRigthClick(messageId){\n            var menuSetting = this.showMessageRightMenu.find(setting => setting.messageId == messageId)\n            if(menuSetting){\n               menuSetting.show = true\n            } else {\n                this.showMessageRightMenu.push({\n                    messageId: messageId,\n                    show: true\n                })\n            }\n        },\n        isShowMessageMenu(item){\n            var menuSetting = this.showMessageRightMenu.find(setting => setting.messageId == item.messageId)\n            if(menuSetting){\n                return menuSetting.show;\n            } else {\n                return false;\n            }\n           \n        },\n        imageThumnailSrc(item){\n            var thumbnail = item.content.binaryContent;\n            if(thumbnail && thumbnail != ''){\n                thumbnail = \"data:image/png;base64,\" +item.content.binaryContent;\n            } else {\n                thumbnail = ''\n            }\n            return thumbnail \n        },\n    },\n    filters: {\n            // 将日期过滤为 hour:minutes\n            time (date) {\n                if (typeof date === 'string') {\n                    date = new Date(date);\n                }\n                if(typeof date === 'number'){\n                   date = new Date(date);\n                }\n                if(date.getMinutes()<10){\n                  return date.getHours() + ':0' +date.getMinutes();\n                }else{\n                  return date.getHours() + ':' + date.getMinutes();\n                }\n            },\n\n            getTimeStringAutoShort2(timestamp){\n                return TimeUtils.getTimeStringAutoShort2(timestamp,true);\n            }\n\n\n    }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n   .receive-image\n      max-width : 115px;\n      max-height : 330px;\n      text-align: center\n      border-radius: 3px\n      \n   .message\n      position: relative\n      width: 100%\n      height: 75%\n      .header\n        height: 14%\n        max-height: 60px\n        min-height: 60px \n        padding: 0px 0 0 30px\n        box-sizing: border-box\n        display:flex\n        .friendname\n            display: flex\n            align-items: center\n            font-size: 18px\n        .friend-group-info\n            padding: 0px 20px 0px 0px\n            display: flex\n            align-items: center\n            margin-left : auto\n            .icon\n                font-size: 24px\n                cursor: pointer \n      .message-wrapper\n        height: 86%\n        padding: 10px 15px\n        box-sizing: border-box\n        overflow-y: auto\n        border-top: 1px solid #e7e7e7\n        border-bottom: 1px solid #e7e7e7\n        background: #f2f2f2\n        .loading\n            color: #9ea0a3;\n            font-size: 12px;\n            font-family: arial;\n            text-align: center;\n            line-height: 30px;\n            img\n                display: inline-block;\n                vertical-align: top;\n                margin: 6px 3px 0 0;\n                height: 18px;\n        .message\n            margin-bottom: 15px\n        .time\n            width: 100%\n            font-size: 12px\n            margin: 7px auto\n            text-align: center\n            span\n                display: inline-block\n                padding: 4px 6px\n                color: #fff\n                border-radius: 3px\n                background-color: #dcdcdc\n        .main\n            margin-top: 10px\n            .avatar \n                float: left\n                margin-left: 15px\n                border-radius: 3px\n            .content \n                display:inline-block\n                width: 65%   \n                .display-name\n                    margin-left: 10px\n                    margin-bottom: 5px\n                    font-size: 8px\n                    color: #999\n                .content-message-right-menu\n                    position: relative    \n                    .content-message\n                        display: inline-block\n                        margin-left: 10px\n                        position: relative\n                        padding: 6px 10px\n                        max-width: 90%\n                        min-height: 36px\n                        line-height: 24px\n                        box-sizing: border-box\n                        font-size: 14px\n                        text-align: left\n                        word-break: break-all\n                        background-color: #fafafa\n                        border-radius: 4px\n                        .attachment\n                            min-width: 200px\n                            max-width: 350px\n                            .ico-bg\n                                background: url(/static/images/icon__attachment-white.png) no-repeat center #3aa4dd;\n                                background-size: 20px; \n                                display: inline-block; \n                                vertical-align: top; \n                                height: 40px; \n                                width: 40px;\n                            .file-info\n                                font-size: 14px;\n                                font-family: \"Micrsofot Yahei\";\n                                margin-left: 10px;\n                            .name\n                                overflow: hidden;\n                                white-space: nowrap; \n                                text-overflow: ellipsis; \n                                max-width: 248px;\n                            .size\n                                color: #666;\n                                font-size: 12px;\n                            .btn-down\n                                background: url(/static/images/icon__download.png) no-repeat center; \n                                background-size: 15px; \n                                display: inline-block; \n                                vertical-align: top; \n                                height: 30px; \n                                width: 20px;                \n                        .text\n                            white-space: pre-wrap;\n                        &:before\n                            content: \" \"\n                            position: absolute\n                            top: 12px\n                            right: 100%\n                            border: 6px solid transparent\n                            border-right-color: #fafafa\n        .self\n            text-align: right\n            .avatar\n                float: right\n                margin:0 15px\n            .content\n                .send-status\n                    height: 100%\n                    display: inline-block\n                    .icon\n                        display: block\n                        font-size: 16px \n                    .icon-fasongshibai\n                        color: red\n                    .icon-loading-solid\n                        animation: changeright 1s linear infinite\n                .content-message-right-menu            \n                    .content-message \n                        background-color: #b2e281\n                        &:before \n                            right: -12px\n                            vertical-align: middle\n                            border-right-color: transparent\n                            border-left-color: #b2e281\n    @keyframes changeright     \n    0% \n        -webkit-transform:rotate(0deg)\n    50%\n        -webkit-transform:rotate(180deg)\n    100%\n        -webkit-transform:rotate(360deg)                      \n                    \n</style>\n"
  },
  {
    "path": "src/components/mycard/mycard.vue",
    "content": "<!-- 最左边的选择框 -->\n<template>\n\t<div class=\"mycard\" ref=\"mycardRef\">\n\t    <header>\n\t    \t<img :src=\"user.img\" class=\"avatar\" @click=\"showPersonCard\" @dblclick=\"changeFullScreenMode\">\n\t\t\t<personcard v-if=\"showPersonalCard\" v-bind:userId=\"userId\"></personcard>\n\t    </header>\n\t    <div class=\"navbar\" @click=\"clearSearch\">\n\t\t\t<div class=\"conversation-item\">\n\t\t\t\t<span v-if=\"unreadTotalCount > 0\" class=\"unread-num\">\n\t\t\t\t\t<span class=\"unread-num-show\">{{unreadTotalCount}}</span>\n\t\t\t\t</span>\n\t\t\t\t<router-link to=\"/conversation\" class=\"icon iconfont icon-dkw_xiaoxi\" >\n\t\t\t\t</router-link>\n\t\t\t</div>\n\t\t\t<div class=\"friend-item\">\n\t\t\t\t<span v-if=\"newFriendRequestCount > 0\" class=\"unread-friend-request-num\">\n\t\t\t\t</span>\n\t\t\t\t<router-link to=\"/friend\" class=\"icon iconfont icon-pengyou\">\t\n\t\t\t\t</router-link>\n\t\t\t</div>\n\t\t\t\n\t\t\t<div class=\"icon iconfont icon-jiahaoyou\" @click=\"showAddRequestTip = !showAddRequestTip\">\n\t\t\t\t<addtip v-show=\"showAddRequestTip\"></addtip>\n\t\t\t</div> \n\t    </div>\n\t    <footer>\n\t        <i title = \"退出\" class=\"icon iconfont icon-tuichu\" @click=\"loginOut\"></i>\n\t    </footer>\n\t</div>\n</template>\n\n<script>\nimport { mapState, mapGetters } from 'vuex'\nimport addtip from '../../components/menu/addtip'\nimport personcard from '../../components/menu/personalCard'\nimport webSocketCli from '../../websocket/websocketcli'\nimport Logger from '../../websocket/utils/logger'\n\nexport default {\n\tcomponents: {\n\t\taddtip,\n\t\tpersoncard\n\t},\n\tdata() {\n\t\treturn {\n\t\t\tshowAddRequestTip: false,\n\t\t\tshowPersonalCard: false\n\t\t}\n\t},\n    computed: {\n       ...mapState([\n\t\t\t 'user',\n\t\t\t 'userId',\n\t\t\t 'newFriendRequestCount'\n\t   ]),\n\t   ...mapGetters([\n\t\t   'unreadTotalCount',\n\t   ])\n    },\n    methods: {\n\t\tshowPersonCard(){\n\t\t\tthis.showPersonalCard = !this.showPersonalCard\n\t\t},\n    \tclearSearch() {\n    \t\tthis.$store.dispatch('search', '')\n\t\t},\n\t\tloginOut(){\n\t\t\tthis.$store.dispatch('loginOut','');\n\t\t},\n\t\tchangeFullScreenMode(){\n\t\t\tvar fullscreen = this.$store.state.changeFullScreenMode;\n\t\t\tconsole.log('change screen mode '+fullscreen);\n\t\t\tvar _this = this;\n\t\t\t_this.$store.state.changeFullScreenMode = !fullscreen;\n\t\t\tsetTimeout(() => {\n\t\t\t\tlet mycardHeight= _this.$refs.mycardRef.offsetHeight;\n\t\t\t\tconsole.log('resize mycard height '+mycardHeight);\n\t\t\t\t_this.$store.state.appHeight = mycardHeight;\n\t\t\t},200);\n\t\t\t\n\t\t}\n\t},\n\t\n\tmounted(){\n\t\tdocument.addEventListener(\"click\",e=>{\n\t\t\tvar isString = typeof(e.target.className) == 'string'\n\t\t\tif (isString && e.target.className.search('icon-jiahaoyou') == -1 && e.target.className !== 'add-content' && this.showAddRequestTip) {\n                this.showAddRequestTip = false;\n\t\t\t}\n\t\t\tlet personCardDom = document.getElementById(\"personal-card\");\n\t\t\tif (isString && e.target.className.search('avatar') == -1 && personCardDom && !personCardDom.contains(event.target) && this.showPersonalCard) {\n                this.showPersonalCard = false;\n            }\n\t\t});\n\t}\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n@import '../../assets/fonts/iconfont.css'\n.mycard\n    position: relative\n    width: 100%\n    height: 100%\n    .avatar\n\t    width: 36px\n\t    height: 36px\n\t    margin: 20px 12px 0 12px\n\t    border-radius: 2px\n    .navbar\n        width: 100%\n        text-align: center\n\t    .icon\n\t        display: inline-block\n\t        font-size: 26px\n\t        margin-top: 28px\n\t        padding: 0 16px\n\t        box-sizing: border-box\n\t        color: rgb(173,174,175)\n\t        opacity: 1\n\t        cursor: pointer\n\t        &.active\n\t            color: rgb(0,220,65)\n\t        &:hover\n\t            opacity: 1;\n\t    .icon-msg,.icon-more\n\t        font-size: 22px\n\t    .icon-msg\n\t        padding: 0 19px\n\t.friend-item\n\t    position:relative\n\t\t.unread-friend-request-num\n\t\t\tdisplay: inline-block\n\t\t\tmin-width: 5px\n\t\t\theight: 5px\n\t\t\tbackground-color: red\n\t\t\tborder-radius: 8px\n\t\t\ttext-align: center\n\t\t\tfont-size: 12px\n\t\t\tcolor: #fff\n\t\t\tline-height: 16px\n\t\t\tposition:absolute\n\t\t\ttop: 25px\n\t\t\tright: 17px\n\t\t\tz-index: 10\t\n\t.conversation-item\n\t    position:relative\n\t\t.unread-num\n\t\t\tdisplay: inline-block\n\t\t\tmin-width: 16px\n\t\t\theight: 16px\n\t\t\tbackground-color: red\n\t\t\tborder-radius: 8px\n\t\t\ttext-align: center\n\t\t\tfont-size: 12px\n\t\t\tcolor: #fff\n\t\t\tline-height: 16px\n\t\t\tposition:absolute\n\t\t\ttop: 20px\n\t\t\tright: 17px\n\t\t\tz-index: 10\n\t\t\t.unread-num-show\n                font-size:10px;\n                -webkit-transform:scale(0.8);\n                display:block;\n\tfooter\n\t    position: absolute\n\t    bottom: 20px\n\t    width: 100%\n\t    text-align: center\n</style>\n"
  },
  {
    "path": "src/components/search/search.vue",
    "content": "<!-- 搜索框 -->\n<template>\n<div class=\"wrapper\">\n\t<div class=\"padding-wrapper\">\n\t\t<div class=\"search-wrapper\">\n\t\t\t<input type=\"text\" class=\"searchInput\" v-model=\"search\" @keyup=\"change\" placeholder=\"搜索\">\n\t\t\t<i class=\"icon iconfont icon-sousuo\" v-show=\"noText\"></i>\n\t\t\t<div class=\"searchInput-delete\" v-show=\"haveText\" @click=\"del\"></div>\n\t\t</div>\n\t</div>\n\n</div>\n</template>\n\n<script>\nexport default {\n\t methods: {\n        change () {\n        \tthis.$store.dispatch('search', this.search)\n        },\n        del () {\n            this.search= ''\n            this.change()\n        }\n     },\n     data () {\n     \treturn {\n     \t\tsearch: '',\n     \t\tactive: false\n     \t}\n     },\n     computed: {\n        noText () {\n   \t      if(this.search  === '') return true\n   \t      return false\n        },\n        haveText () {\n   \t      if(this.search  === '') return false\n   \t      return true\n        }\n\t }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.wrapper\n    max-height: 60px\n    min-height: 60px \n    height: 60px\n\t.padding-wrapper\n\t    border-right: 1px solid #e7e7e7\n\t    padding: 22px 12px 12px 12px\n\t\t.search-wrapper\n\t\t\tposition: relative\n\t\t\tdisplay: flex\n\t\t\tbox-sizing: border-box\n\t\t\theight: 100%\n\t\t\tmax-height: 26px\n\t\t\twidth: 100%\n\t\t\tbackground-color: #e5e3e2\n\t\t\tborder: 1px solid #d9d7d6\n\t\t\tborder-radius: 2px\n\t\t\t.searchInput\n\t\t\t\tflex: 1\n\t\t\t\tfont-size: 12px\n\t\t\t\tpadding: 6px\n\t\t\t\tbackground-color: #e5e3e2\n\t\t\t\toutline: none\n\t\t\t\t&:focus\n\t\t\t\t\tbackground-color: #f2efee\n\t\t\t.icon-sousuo\n\t\t\t\tdisplay: inline-block\n\t\t\t\twidth: 24px\n\t\t\t\theight: 24px\n\t\t\t\tfont-size: 14px\n\t\t\t\tline-height: 24px\n\t\t\t\ttext-align: center\n\t\t\t.searchInput-delete\n\t\t\t\tdisplay: block\n\t\t\t\tposition: absolute\n\t\t\t\toutline: none\n\t\t\t\ttop: 0;\n\t\t\t\tright: 0;\n\t\t\t\twidth: 24px\n\t\t\t\theight: 100%\n\t\t\t\tbackground-image: url(delete.png)\n\t\t\t\tbackground-size: 26px\n\t\t\t\tbackground-position: center\n\t\t\t\tbackground-repeat: no-repeat\n\t\t\t\tcursor: pointer\n</style>\n"
  },
  {
    "path": "src/components/text/text.vue",
    "content": "<!-- 文本输入框 -->\n<template>\n<div class=\"text\">\n    <div class=\"emoji\">\n        <i class=\"icon iconfont icon-biaoqing\" @click=\"showEmoji=!showEmoji\"></i>\n        <i title=\"语音聊天\" class=\"icon iconfont icon-dianhua\" v-show=\"isSingleConversation\" @click=\"sendAudio\"></i>\n        <i title=\"视频聊天\" class=\"icon iconfont icon-ai-video\"  @click=\"sendVideo\"></i>\n        <i title=\"发送图片\" class=\"icon iconfont icon-tupian\" >\n            <input type=\"file\" accept=\"image/*\" id=\"chat-send-img\" ref=\"uploadPic\" @change=\"sendPic\">\n        </i>\n        <i title=\"发送视频\" class=\"icon iconfont icon-shipin\" >\n            <input type=\"file\" accept=\"video/*\" id=\"chat-send-video\" ref=\"uploadVideo\" @change=\"sendVideoMessage\">\n        </i>\n        <i title=\"发送文件\" class=\"icon iconfont icon-wenjian\">\n            <input type=\"file\" accept=\"*\" id=\"chat-send-file\" ref=\"uploadFile\" @change=\"sendFile\">\n        </i>\n        <transition name=\"showbox\">\n            <div class=\"emojiBox\" v-show=\"showEmoji\">\n                <li v-bind:key= index v-for=\"(item, index) in emojis\">\n                    <img :src=\"'static/emoji/'+item.file\" :data=\"item.code\" @click=\"content +=item.code\">\n                </li>\n            </div>\n        </transition>\n\n        <transition name=\"voice-video-chat-box\">\n            <div class=\"chat-modal\" v-show=\"false\">\n                <div class=\"chat-box\">\n                    <video id=\"video-remote\" playsinline autoplay muted></video>\n                    <video id=\"video-local\" playsinline autoplay muted></video>\n                </div>\n            </div>\n\n        </transition>\n\n        \n        <div class=\"callContent\" v-show=\"showChatBox\">\n            <div class=\"\">\n                <div class=\"callercontent callshow\" style=\"\">\n                    <div class=\"exchange-content\">\n                        <div class=\"playcontent left-big-content\">\n                            <img id=\"wxCallRemoteImg\" class=\"bigavatar\" :src=\"callRemoteImg\" v-show=\"showCallRemoteImg\"/> \n                            <p id=\"wxCallTips\" class=\"calltips\" v-text=\"videoTextCallTips\" v-show=\"showCallTips\"> 接通中... </p> \n                            <video id=\"wxCallRemoteVideo\" autoplay=\"autoplay\" playsinline=\"\" style=\"display: none;\" v-show=\"showCallRemoteVideo\"></video>\n                        </div> \n                        <div class=\"playcontent right-sml-content\">\n                            <img id = \"wxCallLocalImg\" :src=\"callLocalImg\" class=\"bigavatar\" v-show=\"showCallLocalImg\"/> \n                            <video id=\"wxCallLocalVideo\" autoplay=\"autoplay\" muted=\"muted\" playsinline=\"\" style=\"display: none;\" v-show=\"showCallLocalVideo\"></video>\n                        </div>\n                    </div> \n                    <div class=\"opera-content flexbox\">\n                        <img class=\"calleravatar\" :src=\"callRemoteImg\" /> \n                        <span class=\"flexauto overell callnick\" v-text=\"callDisplayName\"></span> \n                        <span class=\"flexbox\">\n                            <span class=\"operabtn canclecall btnopacity\" v-show=\"cancelCall\" @click=\"cancel\">取消</span> \n                            <span class=\"operabtn canclecall btnopacity\" style=\"display: none;\" v-show=\"rejectCall\" @click=\"reject\">拒绝</span> \n                            <span class=\"operabtn upcall btnopacity\" style=\"display: none;\" v-show=\"acceptCall\" @click=\"accept\">接听</span>\n                        </span> \n                        <span class=\"talktime flexbox\" style=\"display: none;\" v-show=\"showTalkTime\"><i class=\"iconfont icon-ai-video\"></i> <span v-text=\"talkTime\">00:00</span></span> \n                        <span class=\"operabtn canclecall btnopacity\" style=\"display: none;\" v-show=\"hangUpCall\" @click=\"hangUp\"><i class=\"iconfont icon-guaduan\"></i>挂断 </span> \n                        <button class=\"screenbtn\"><i class=\"iconfont icon-quanping iconfull\" style=\"display: none;\"></i></button> \n                        <button class=\"screenbtn\" style=\"display: none;\"><i class=\"iconfont icon-tuichuquanping iconfull\"></i></button>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"audioContent\" v-show=\"showAudioBox\">\n            <div class=\"audioBody callshow\" style=\"\">\n                <div class=\"audioBg\">\n                    <img class=\"callavatar\" :src=\"callRemoteImg\" /> \n                    <div class=\"blackbg\"></div>\n                </div> \n                <div class=\"audiomain\">\n                    <img class=\"audio-avatar\" :src=\"callRemoteImg\" /> \n                    <p class=\"callnick\" v-text=\"callDisplayName\"></p> \n                    <p class=\"call-time\" style=\"display: none;\" v-show=\"showTalkTime\" v-text=\"talkTime\">00:00</p> \n                    <p class=\"waiting-msg\" v-show=\"waitingMsg\" v-text=\"waitingMsgTips\"> 接通中... </p> \n                    <div class=\"call-opera flexbox\">\n                        <span class=\"cancleaudio btnopacity\" style=\"display: none;\" v-show=\"hangUpCall\" @click=\"hangUp\"><i class=\"iconfont icon-guaduan\"></i>挂断 </span> \n                        <div class=\"loadingcall flexbox\">\n                            <span class=\"cancleaudio callercanle btnopacity\" style=\"display: none;\" v-show=\"cancelCall\" @click=\"cancel\"><i class=\"iconfont icon-guaduan\"></i>取消 </span> \n                            <span class=\"cancleaudio btnopacity\" style=\"display: none;\" v-show=\"rejectCall\" @click=\"reject\">拒绝</span> \n                            <span class=\"upcall btnopacity\" style=\"display: none;\" v-show=\"acceptCall\" @click=\"accept\">接听</span>\n                        </div>\n                    </div>\n                </div> \n                <div style=\"display: none;\">\n                    <audio id=\"wxCallRemoteAudio\" autoplay=\"autoplay\"></audio>\n                </div> \n                <div style=\"display: none;\">\n                    <audio id=\"wxCallLocalAudio\" autoplay=\"autoplay\" muted=\"muted\"></audio>\n                </div>\n            </div>\n        </div>\n\n    </div>\n    <textarea id=\"sendText\" ref=\"text\" v-model=\"content\" @keydown.enter.exact=\"sendMessage\" @keydown.ctrl.enter=\"newline\" @focus=\"onFocus\" @click=\"showEmoji=false\"></textarea>\n    <div class=\"send\" @click=\"send\" ref=\"sendBtn\" v-bind:class=\"{disable : sendBtnDisabled}\">\n    \t<span>发送</span>\n    </div>\n    <transition name=\"appear\">\n\t    <div class=\"warn\" v-show=\"warn\">\n\t    \t<div class=\"description\">不能发送空白信息</div>\n\t    </div>\n\t</transition>\n</div>\n</template>\n\n<script>\nimport adapter from 'webrtc-adapter'\nimport { mapGetters, mapState } from 'vuex'\nimport TextMessageContent from '../../websocket/message/textMessageContent'\nimport ImageMessageContent from '../../websocket/message/imageMessageContent'\nimport * as qiniu from 'qiniu-js'\nimport MessageContentMediaType from '../../websocket/message/messageContentMediaType'\nimport LocalStore from '../../websocket/store/localstore'\nimport SessionCallback from '../../webrtc/sessionCallback'\nimport EngineCallback from '../../webrtc/engineCallback'\nimport SendMessage from '../../websocket/message/sendMessage'\nimport CallState from '../../webrtc/callState'\nimport {UPLOAD_BY_QINIU, SUCCESS_CODE } from '../../constant'\nimport webSocketCli from '../../websocket/websocketcli'\nimport Message from '../../websocket/message/message'\nimport ProtoMessage from '../../websocket/message/protomessage'\nimport VideoMessageContent from '../../websocket/message/videoMessageContent'\nimport FileMessageContent from '../../websocket/message/fileMessageContent'\nexport default {\n    data () {\n        return {\n            content: '',\n            sendBtnDisabled: true,\n            reply: '未找到',  \n            frequency: 0,\n            warn: false,\n            showEmoji: false,\n            videoTextCallTips: '',\n            voipClient: null,\n            rejectCall: false,\n            cancelCall: false,\n            acceptCall: false,\n            hangUpCall: false,\n            showCallLocalImg: true,\n            showCallLocalVideo: false,\n            showCallRemoteImg: true,\n            showCallRemoteVideo: false,\n            showCallTips: true,\n            callRemoteImg: 'static/images/vue.jpg',\n            callLocalImg: 'static/images/vue.jpg',\n            callDisplayName: '',\n            waitingMsg: false,\n            isAudioOnly: false,\n            waitingMsgTips: '',\n            showTalkTime: false,\n            talkInterval: 0,\n            talkTime: '00:00',\n            talkTimerInterval: null,\n        };\n    },\n    computed: {\n        ...mapState([   \n            'selectId',\n            'emojis',\n            'showChatBox',\n            'showAudioBox',\n            'userInfoList',\n            'inCommingNotify',\n            'outGoingNotify'\n        ]),\n        ...mapGetters([\n            'selectedChat',\n            'isSingleConversation',\n        ])\n    },\n    methods: {\n        sendPic(e){\n           var store = this.$store;\n           var file = e.target.files[0];\n           var localPath = e.target.value\n           if(UPLOAD_BY_QINIU){\n                store.dispatch('getUploadToken', MessageContentMediaType.Image);\n                console.log(\"sendpic \"+e.target.value);\n                var key = MessageContentMediaType.Image +\"-\"+LocalStore.getUserId()+\"-\"+new Date().getTime()+\"-\"+file.name;\n                setTimeout(()=> {\n                        var token = LocalStore.getImageUploadToken();\n                        console.log(\"upload file key \"+key+\" token \"+token);\n                        if(token){\n                            var observable = qiniu.upload(file, key, token, null, null);\n                            var observer = {\n                                    next(res){\n                                        console.log('uploading '+res.total.percent);\n                                    },\n                                    error(err){\n                                        console.log(\"upload error \"+err.code +\" message \"+err.message);\n                                    }, \n                                    complete(res){\n                                        console.log(\"upload complete \"+res);\n                                        var localPath = e.target.value;\n                                        var remotePath = \"http://image.comsince.cn/\"+key;\n                                        var imageMessageContent = new ImageMessageContent(localPath,remotePath,null);\n                                        store.dispatch('sendMessage', new SendMessage(null,imageMessageContent))\n                                    }\n                                }\n                            observable.subscribe(observer);\n                        }\n                        \n                },200);\n           } else {\n                this.sendImage(file)  \n           }\n           this.$refs.uploadPic.value = null;\n        },\n\n        sendImage(file){\n            var store = this.$store;\n            var key = MessageContentMediaType.Image +\"-\"+LocalStore.getUserId()+\"-\"+new Date().getTime()+\"-\"+file.name;\n            webSocketCli.getMinioUploadUrl(MessageContentMediaType.Image,key).then(data => {\n                if(data.code == SUCCESS_CODE){\n                    console.log(\"domain \"+data.result.domain+\" url \"+data.result.url)\n                    var messageId;\n                    var thunmbanilwithoutDesc;\n                    //获取缩略图,同时也为了适配android 端适配的问题，防止转发图片报错\n                    var reader = new FileReader()\n                    reader.readAsDataURL(file)\n                    reader.onload = (e) => {\n                        var result = e.target.result\n                        this.canvasDataURL(result,{   \n                        },base64Img => {\n                            thunmbanilwithoutDesc = base64Img.split(',')[1]\n                            //添加缩略消息\n                            var imageMessageContent = new ImageMessageContent(file,null,thunmbanilwithoutDesc);\n                            var message = Message.conert2Message(new SendMessage(null,imageMessageContent));\n                            var protoMessage = ProtoMessage.convertToProtoMessage(message);\n                            messageId = protoMessage.messageId\n                            store.dispatch('preAddProtoMessage', protoMessage);\n                        })\n                    }\n\n                    fetch(data.result.url, {\n                        method: 'PUT',\n                        body: file\n                        }).then(() => {\n                            var remotePath = data.result.domain+\"/\"+key;\n                            console.log(\"remote path \"+remotePath)\n                            var imageMessageContent = new ImageMessageContent(file,remotePath,thunmbanilwithoutDesc);\n                            store.dispatch('updateSendMessage', {messageId: messageId,messageContent:imageMessageContent})\n                        }).catch((e) => {\n                            console.error(e);\n                        });\n                }\n            }) \n        },\n \n        /*** js 图片压缩上传(纯js的质量压缩，非长宽压缩) \n         * https://www.cnblogs.com/xiangsj/p/8932525.html\n        */\n        canvasDataURL(path, obj, callback){\n            var img = new Image();\n            img.src = path;\n            img.onload = function(){\n                var that = this;\n                // 默认按比例压缩\n                var w = that.width,\n                    h = that.height,\n                    scale = w / h;\n                w =  w * 0.5 > 113 ? 113 : w * 0.5;\n                h =  w / scale;\n                console.log(\"scale \"+scale+\" transfer size \"+w+\":\"+h)\n                var quality = 0.5;  // 默认图片质量为0.7\n                //生成canvas\n                var canvas = document.createElement('canvas');\n                var ctx = canvas.getContext('2d');\n                // 创建属性节点\n                var anw = document.createAttribute(\"width\");\n                anw.nodeValue = w;\n                var anh = document.createAttribute(\"height\");\n                anh.nodeValue = h;\n                canvas.setAttributeNode(anw);\n                canvas.setAttributeNode(anh);\n                ctx.drawImage(that, 0, 0, w, h);\n                // 图像质量\n                if(obj.quality && obj.quality <= 1 && obj.quality > 0){\n                    quality = obj.quality;\n                }\n                // quality值越小，所绘制出的图像越模糊\n                var base64 = canvas.toDataURL('image/jpeg', quality);\n                // 回调函数返回base64的值\n                callback(base64);\n            }\n        },\n\n        //https://github.com/metroluffy/blog/issues/18\n        sendVideoMessage(e){\n            var store = this.$store;\n            var file = e.target.files[0];\n            var localPath = e.target.value\n\n            var key = MessageContentMediaType.Video +\"-\"+LocalStore.getUserId()+\"-\"+new Date().getTime()+\"-\"+file.name;\n            webSocketCli.getMinioUploadUrl(MessageContentMediaType.Video,key).then(data => {\n                    if(data.code == SUCCESS_CODE){\n                        console.log(\"domain \"+data.result.domain+\" url \"+data.result.url)\n                        var messageId;\n                        var thunmbanilwithoutDesc;\n                        //获取缩略图,同时也为了适配android 端适配的问题，防止转发图片报错\n                        var url = URL.createObjectURL(file);\n                        console.log(\"video url \"+url)\n\n                        this.getVideoImage(url,base64Img =>{\n                            console.log(\"base64Img \"+base64Img)\n                            thunmbanilwithoutDesc = base64Img.split(',')[1]\n                                //添加缩略消息\n                                var videoMessageContent = new VideoMessageContent(localPath,'',thunmbanilwithoutDesc);\n                                var message = Message.conert2Message(new SendMessage(null,videoMessageContent));\n                                var protoMessage = ProtoMessage.convertToProtoMessage(message);\n                                messageId = protoMessage.messageId\n                                store.dispatch('preAddProtoMessage', protoMessage);\n                            },2)\n\n                        fetch(data.result.url, {\n                            method: 'PUT',\n                            body: file\n                            }).then(() => {\n                                var remotePath = data.result.domain+\"/\"+key;\n                                console.log(\"remote path \"+remotePath)\n                                var imageMessageContent = new VideoMessageContent(localPath,remotePath,thunmbanilwithoutDesc);\n                                store.dispatch('updateSendMessage', {messageId: messageId,messageContent:imageMessageContent})\n                            }).catch((e) => {\n                                console.error(e);\n                            });\n                    }\n                }) \n\n            this.$refs.uploadVideo.value = null;\n        },\n\n        getVideoImage(path, callback, secs = 1) {\n            var me = this,\n            video = document.createElement('video');\n            video.onloadedmetadata = function () {\n                if ('function' === typeof secs) {\n                    secs = secs(this.duration);\n                }\n                this.currentTime = Math.min(Math.max(0, (secs < 0 ? this.duration : 0) + secs), this.duration);\n                console.log(\"secs \"+secs+\" currentTime \"+this.currentTime)\n            };\n            video.onseeked = function (e) {\n                var canvas = document.createElement('canvas');\n                var w = video.videoHeight,\n                    h = video.videoWidth,\n                    scale = w / h;\n                w =  w  > 250 ? 250 : w;\n                h =  w / scale;\n                console.log(\"scale \"+scale+\" transfer size \"+w+\":\"+h)\n                canvas.height = h;\n                canvas.width = w;\n                var ctx = canvas.getContext('2d');\n                ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n                var imgBase64 = canvas.toDataURL('image/jpeg', 0.7);\n                callback(imgBase64);\n            };\n            video.onerror = function (e) {\n                console.log(\"excption\"+e)\n                callback.call(me, undefined, undefined, e);\n            };\n            video.src = path;\n        },\n\n        sendFile(e){\n            var store = this.$store;\n            var file = e.target.files[0];\n            var localPath = e.target.value\n\n            var key = MessageContentMediaType.File +\"-\"+LocalStore.getUserId()+\"-\"+new Date().getTime()+\"-\"+file.name;\n            webSocketCli.getMinioUploadUrl(MessageContentMediaType.File,key).then(data => {\n                if(data.code == SUCCESS_CODE){\n                    console.log(\"domain \"+data.result.domain+\" url \"+data.result.url)\n                    var messageId;\n                    var fileMessageContent = new FileMessageContent(file,'');\n                    var message = Message.conert2Message(new SendMessage(null,fileMessageContent));\n                    var protoMessage = ProtoMessage.convertToProtoMessage(message);\n                    messageId = protoMessage.messageId\n                    store.dispatch('preAddProtoMessage', protoMessage);\n            \n\n                    fetch(data.result.url, {\n                        method: 'PUT',\n                        body: file\n                        }).then(() => {\n                            var remotePath = data.result.domain+\"/\"+key;\n                            console.log(\"remote path \"+remotePath)\n                            var message = new FileMessageContent(file,remotePath);\n                            store.dispatch('updateSendMessage', {messageId: messageId,messageContent:message})\n                        }).catch((e) => {\n                            console.error(e);\n                        });\n                }\n                }) \n\n            this.$refs.uploadFile.value = null;\n        },\n\n        // 按回车发送信息\n        sendMessage (e) {\n            console.log(\"send code \"+e.keyCode);\n            if(e.keyCode === 13){\n              this.send();\n              //阻止回车换行\n              e.preventDefault();\n            }\n            \n        },\n\n        newline(e){\n           this.content = this.content + '\\n';\n           e.preventDefault();\n        },\n\n        onBlur(){\n           console.log('send text onblur');\n        },\n\n        onFocus(){\n           console.log('send text onfoucs');\n           this.$store.dispatch('clearUnreadStatus', '');\n        },\n        // 点击发送按钮发送信息\n        send () {\n            console.log(\"send message \"+this.content);\n            if(this.content.length < 1){\n                this.warn = true\n                this.content = ''\n                setTimeout(() => {\n                    this.warn = false;\n                  }, 1000)\n               }else{\n                    //进行消息类型包装\n                    var textMessageContent = new TextMessageContent(this.content);\n                    this.sendMessageToStore(new SendMessage(null,textMessageContent));\n                    this.content = '';\n                    this.$refs.text.focus();\n               }\n        },\n        //发送语音聊天\n        sendAudio(){\n            this.$store.state.showAudioBox = true;\n            this.waitingMsg = true;\n            this.rejectCall = false;\n            this.acceptCall = false;\n            this.hangUpCall = false;\n            this.cancelCall = true;\n            this.initCallUserInfo(this.$store.state.selectTarget);\n            this.isAudioOnly = true;\n            this.voipClient.startCall(this.$store.state.selectTarget,this.isAudioOnly);\n        },\n        //发送视频聊天\n        sendVideo(){\n            if(this.isSingleConversation){\n                this.$store.state.showChatBox = true;\n                this.rejectCall = false;\n                this.acceptCall = false;\n                this.hangUpCall = false;\n                this.cancelCall = true;\n                this.showCallRemoteVideo = false;\n                this.showCallRemoteImg = true;\n                this.showCallTips = true;\n                this.videoTextCallTips = \"正在接通，请稍候...\";\n                this.initCallUserInfo(this.$store.state.selectTarget);\n                this.isAudioOnly = false;\n                this.voipClient.startCall(this.$store.state.selectTarget,this.isAudioOnly);\n            } else {\n                this.$store.state.groupOperateState = 4;\n                //触发groupMap以是vue相应变更\n                this.$store.state.groupMemberTracker += 1;\n                this.$store.state.showCreateGroupDialog = true;\n            }\n            \n        },\n        initCallUserInfo(target){\n            var portrait = this.getUserPortrait(target);\n            if(portrait){\n                this.callRemoteImg = portrait;\n            }\n            this.callLocalImg = this.$store.state.user.img;\n            this.callDisplayName = this.getDisplayName(target);\n        },\n        getUserPortrait(target){\n            var userInfo = this.userInfoList.find(userInfo => userInfo.uid == target);\n            if(userInfo){\n                return userInfo.portrait;\n            }\n            return null;\n        },\n        getDisplayName(target){\n            var userInfo = this.userInfoList.find(userInfo => userInfo.uid == target);\n            return userInfo.displayName;\n        },\n        hangUp(){\n            this.voipClient.cancelCall();\n        },\n        reject(){\n           this.voipClient.cancelCall();\n        },\n        accept(){\n           this.rejectCall = false;\n           this.acceptCall = false;\n           this.hangUpCall = true;\n           this.voipClient.answerCall(this.isAudioOnly);\n        },\n        cancel(){\n           this.voipClient.cancelCall();\n        },\n        sendMessageToStore(sendMessage){\n           this.$store.dispatch('sendMessage', sendMessage)\n        }\n    },\n    // 在进入的时候 聚焦输入框\n    mounted() {\n        this.$refs.text.focus()\n        var sessionCallback = new SessionCallback();\n        var engineCallback = new EngineCallback();\n        engineCallback.onReceiveCall = session => {\n            this.isAudioOnly = session.audioOnly;\n            console.log(\"receive isAudioOnly \"+session.audioOnly);\n            if(!this.isAudioOnly){\n                this.$store.state.showChatBox = true;\n                this.videoTextCallTips = '对方邀请您进行视频通话';\n                this.rejectCall = true;\n                this.cancelCall = false;\n                this.acceptCall = true;\n                this.hangUpCall = false;\n                this.showCallLocalImg = true;\n                this.showCallLocalVideo = false;\n                this.showCallRemoteImg = true;\n                this.showCallRemoteVideo = false;\n                this.showCallTips = true;\n                this.initCallUserInfo(session.clientId);\n            } else {\n                this.$store.state.showAudioBox = true;\n                this.waitingMsg = true;\n                this.waitingMsgTips = '对方邀请你进行语音通话';\n                this.rejectCall = true;\n                this.acceptCall = true;\n                this.cancelCall = false;\n                this.hangUpCall = false;\n                this.initCallUserInfo(session.clientId);\n            }\n                    \n        //取消textarea焦点聚焦\n            try {\n                document.getElementById('sendText').blur();\n            } catch(error){\n                console.error(\"get sendText error \"+error);\n            }\n        }\n\n        engineCallback.shouldStartRing = isIncomming => {\n            if(isIncomming){\n                this.inCommingNotify.loopPlay()\n            } else {\n                this.outGoingNotify.loopPlay();\n            }\n        }\n\n        engineCallback.shouldSopRing = () => {\n            this.inCommingNotify.stopPlay();\n            this.outGoingNotify.stopPlay();\n        }\n\n        sessionCallback.didChangeState = state => {\n            if(state === CallState.STATUS_CONNECTED){\n                if(this.isAudioOnly){\n                    this.cancelCall = false;\n                    this.rejectCall = false;\n                    this.acceptCall = false;\n                    this.hangUpCall = true;\n                    this.waitingMsg = false;\n                } else {\n                    this.cancelCall = false;\n                    this.acceptCall = false;\n                    this.rejectCall = false;\n                    this.hangUpCall = true;\n                }\n                this.showTalkTime = true;\n                this.talkTimerInterval = setInterval(() => {\n                    this.talkInterval += 1;\n                    var min = Math.floor(this.talkInterval / 60 % 60);\n                    var sec = Math.floor(this.talkInterval % 60);\n                    sec = sec < 10 ? \"0\"+sec : sec;\n                    min = min < 10 ? \"0\"+min : min;\n                    this.talkTime = min + \":\"+ sec;\n                },1000)\n            }\n        }\n\n        sessionCallback.didReceiveRemoteAudioTrack = stream => {\n            document.getElementById(\"wxCallRemoteAudio\").srcObject = stream;\n        }\n            \n        sessionCallback.didCallEndWithReason = callEndReason => {\n            if(this.isAudioOnly){\n                this.$store.state.showAudioBox = false;\n            } else {\n                this.$store.state.showChatBox = false;\n            }\n            if(this.talkTimerInterval){\n                this.showTalkTime = false;\n                this.talkInterval = 0;\n                clearInterval(this.talkTimerInterval);\n            }\n        }\n\n        sessionCallback.didCreateLocalVideoTrack = (stream) => {\n            this.showCallLocalImg = false;\n            this.showCallLocalVideo = true;\n            document.getElementById(\"wxCallLocalVideo\").srcObject = stream;\n        }\n\n        sessionCallback.didReceiveRemoteVideoTrack = stream => {\n            this.showCallRemoteImg = false;\n            this.showCallTips = false;\n            this.showCallRemoteVideo = true;\n            document.getElementById(\"wxCallRemoteVideo\").srcObject = stream;\n        }\n        this.voipClient = this.$store.state.voipClient;\n        this.voipClient.setCurrentSessionCallback(sessionCallback);\n        this.voipClient.setCurrentEngineCallback(engineCallback);\n\n        var _this = this\n        document.getElementById('sendText').addEventListener('paste',function(e){\n                var cbd = e.clipboardData;\n                var ua = window.navigator.userAgent;\n                // 没有数据\n                if (!(e.clipboardData && e.clipboardData.items)) { return;\n                }\n                // Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉\n                if(cbd.items && cbd.items.length === 2 && cbd.items[0].kind === \"string\" && cbd.items[1].kind === \"file\" &&\n                    cbd.types && cbd.types.length === 2 && cbd.types[0] === \"text/plain\" && cbd.types[1] === \"Files\" &&\n                    ua.match(/Macintosh/i) && Number(ua.match(/Chrome\\/(\\d{2})/i)[1]) < 49){\n                    return;\n                }\n                for(var i = 0; i < cbd.items.length; i++){\n                    var item = cbd.items[i];\n                    var blob;\n                    if(item.kind == \"file\"){\n                        blob = item.getAsFile();  if(blob.size === 0){  return;\n                    }\n                    // 插入图片记录\n                    _this.sendImage(blob)\n                        \n                }\n            }\n        });\n    },\n    watch: {\n        // 在选择其它对话的时候 聚焦输入框\n        selectId() {\n          setTimeout(() => {\n            this.$refs.text.focus()\n          }, 0)\n        },\n        // 当输入框中的值为空时 弹出提示  并在一秒后消失\n        content() {\n            if(this.content === ''){\n                this.$refs.sendBtn.style.background = \"#f5f5f5\";\n                this.$refs.sendBtn.style.color = '#7c7c7c';\n                this.sendBtnDisabled = true;\n            } else {\n                this.$refs.sendBtn.style.background = \"rgb(18,150,17)\";\n                this.$refs.sendBtn.style.color = '#fff';\n                this.sendBtnDisabled = false;\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.text\n    position: relative\n    height: 25%\n    background: #fff\n    .emoji\n        position: relative\n        width: 100%\n        height: 26%\n        line-height: 40px\n        font-size: 12px\n        padding: 0 30px\n        box-sizing: border-box\n        color: #7c7c7c\n        i\n            font-size: 20px;\n            margin-right : 10px;\n            cursor: pointer\n            position: relative\n            &:hover\n                color: #1aad19\n        input\n            opacity: 0;\n            height: 100%;\n            width: 100%;\n            position: absolute;\n            left: 0;\n            top: 0;\n            z-index: 11;\n            font-size: 0;\n            cursor: pointer;        \n        .emojiBox\n            position: absolute\n            display: flex\n            flex-wrap: wrap\n            top: -210px\n            left: -100px\n            width: 300px\n            height: 200px\n            padding: 5px\n            background-color: #fff\n            border: 1px solid #d1d1d1\n            border-radius: 2px\n            box-shadow:0 1px 2px 1px #d1d1d1\n            &.showbox-enter-active, &.showbox-leave-active\n                transition: all .5s\n            &.showbox-enter,&.showbox-leave-active\n                opacity: 0\n        .chat-modal\n            position: fixed;\n            left: 0;\n            top: 0;\n            right: 0;\n            bottom: 0;\n            z-index: 1001;\n            -webkit-overflow-scrolling: touch;\n            outline: 0;\n            overflow: hidden;\n            margin: 30/@rate auto;\n            background-color: rgba(0,0,0,.3);\n            .chat-box\n                position: relative;\n                left: 50%;\n                top: 5%;\n                transform: translate(-50%,0);\n                padding: 50/@rate 40/@rate;\n                background: #fff;\n                height: 800px;\n                width: 480px;\n                .video-local\n                  width: 50px;\n                  height: 100px;\n                  vertical-align: middle;\n                .video-remote  \n                  width: 480px;\n                  height: 800px;\n                  vertical-align: middle;\n        .btnopacity:hover\n\t        opacity: .8          \n        .callContent\n           .callercontent\n                width: 664px;\n                height: 414px;\n                position: absolute;\n                left: 0;\n                right: 0;\n                margin: auto;\n                top: 10;\n                bottom: 0;\n                z-index: 2000\n\n            .callercontent video\n                width: 100%;\n                background: #000\n\n            .callercontent.callnone\n                display: none\n            .callercontent.callshow\n                display: block\n            .left-big-content\n                width: 480px;\n                height: 360px;\n                position: absolute;\n                left: 0;\n                top: 0\n            .left-big-content .bigavatar\n                width: 100%;\n                height: 100%;\n                filter: blur(6px)\n\n            .left-big-content video\n                width: 100%;\n                height: 100%;\n                background: #000\n            .right-sml-content\n                width: 160px;\n                height: 120px;\n                box-shadow: 0 6px 20px 0 rgba(48,52,58,0.5);\n                border-radius: 4px;\n                position: absolute;\n                right: 0;\n                bottom: 0\n            .right-sml-content .bigavatar\n                width: 100%;\n                height: 100%;\n                border-radius: 4px\n            .opera-content\n                padding: 10px 16px;\n                box-shadow: 0 6px 20px 0 rgba(48,52,58,0.5);\n                height: 56px;\n                width: 480px;\n                background: #fff;\n                position: absolute;\n                bottom: 0;\n                left: 0\n            .opera-content .calleravatar\n                width: 36px;\n                height: 36px;\n                margin-right: 16px;\n                flex-shrink: 0;\n                border-radius: 100%\n            .opera-content .callnick\n                color: #30343a\n            .opera-content .operabtn\n                width: 72px;\n                height: 32px;\n                border-radius: 6px;\n                color: #fff;\n                text-align: center;\n                font-size: 12px;\n                line-height: 32px;\n                cursor: pointer\n            .opera-content .canclecall\n                background: #ff6161\n            .opera-content .canclecall .iconfont\n                color: #fff;\n                font-size: 16px;\n                margin-right: 8px\n            .opera-content .upcall\n                margin-left: 16px;\n                background: #39ba70\n            .calltips\n                position: absolute;\n                margin: auto;\n                text-align: center;\n                color: #fff;\n                font-size: 16px;\n                z-index: 10;\n                left: 0;\n                right: 0;\n                top: 0;\n                line-height: 360px\n            .flexshrink\n                flex-shrink: 0\n            .iconfull\n                margin-left: 16px;\n                font-size: 16px;\n                cursor: pointer\n            .screenbtn\n                background: #fff;\n                border: 0\n            .talktime span\n                font-size: 12px;\n                color: #30343a;\n                margin-left: 8px;\n                margin-right: 16px\n            .flexbox\n                display: flex;\n                align-items: center\n            .flexauto\n                flex: 1\n        .audioContent\n            .flexbox\n                display: flex;\n                align-items: center\n            .callshow\n\t            display: block\n            .audioBody\n                width: 280px;\n                height: 344px;\n                position: absolute;\n                left: 0;\n                right: 0;\n                margin: auto;\n                top: 10;\n                bottom: 0;\n                z-index: 2000;\n                border-radius: 3px\n            .audioBody .audioBg\n                position: absolute;\n                width: 100%;\n                height: 100%;\n                left: 0;\n                top: 0;\n                border-radius: 3px\n            .audioBody .audioBg .callavatar\n                width: 100%;\n                height: 100%;\n                filter: blur(4px)\n            .audioBody .audioBg .blackbg\n                width: 100%;\n                height: 100%;\n                position: absolute;\n                z-index: 5;\n                top: 0;\n                left: 0;\n                background: #000;\n                opacity: .5\n            .audioBody .audiomain\n                position: relative;\n                z-index: 6;\n                text-align: center;\n                color: #fff\n            .audioBody .audiomain .audio-avatar\n                width: 73px;\n                height: 73px; \n                position: relative;\n                border-radius: 50%;\n                overflow: hidden;\n                margin-top: 64px;\n                margin-bottom: 12px\n            .audioBody .audiomain .callnick\n                font-size: 16px;\n                line-height: 22px\n            .audioBody .audiomain .call-time\n                line-height: 20px;\n                margin-top: 4px\n            .audioBody .audiomain .call-opera\n                justify-content: center\n            .audioBody .audiomain .call-opera span\n                width: 96px;\n                height: 32px;\n                margin: 66px 12px 0;\n                line-height: 32px;\n                border-radius: 6px;\n                cursor: pointer\n            .audioBody .audiomain .call-opera span .iconfont\n                &:hover\n                   pointer-events: none\n                font-size: 16px;\n                margin-right: 8px\n            .audioBody .audiomain .call-opera .nomuted\n                border: 1px solid #fff\n            .audioBody .audiomain .call-opera .muted\n                background: #fff;\n                color: #30343a\n            .audioBody .audiomain .call-opera .cancleaudio\n                background: #ff6161\n            .audioBody .audiomain .call-opera .callercanle\n                width: 128px\n            .audioBody .loadingcall\n                justify-content: center\n            .audioBody .loadingcall .upcall\n                background: #39ba70             \n    textarea\n        box-sizing: border-box\n        padding: 0 30px\n        height: 74%\n        width: 100%\n        border: none\n        outline: none\n        font-family: \"Micrsofot Yahei\"\n        font-size: 13px\n        resize: none\n    .send\n        position: absolute\n        bottom: 10px\n        right: 30px\n        width: 75px\n        height: 28px\n        line-height: 28px\n        box-sizing: border-box\n        text-align: center\n        border: 1px solid #e5e5e5\n        border-radius: 3px\n        background: #f5f5f5\n        font-size: 14px\n        color: #7c7c7c\n        &:hover\n            background: rgb(18,150,17)\n            color: #fff\n    .warn\n         position: absolute\n         bottom: 50px\n         right: 10px\n         width: 110px\n         height: 30px\n         line-height: 30px\n         font-size: 12px\n         text-align: center\n         border: 1px solid #bdbdbd\n         border-radius: 4px\n         box-shadow:0 1px 5px 1px #bdbdbd\n         &.appear-enter-active, &.appear-leave-active\n            transition: all 1s\n         &.appear-enter,&.appear-leave-active\n            opacity: 0\n         &:before\n            content: \" \"\n            position: absolute\n            top: 100%\n            right: 20px\n            border: 7px solid transparent\n            border-top-color: #fff\n            filter:drop-shadow(1px 3px 2px #bdbdbd)\n    \n    .disable\n        pointer-events: none; \n</style>\n"
  },
  {
    "path": "src/constant/index.js",
    "content": "export const WS_PROTOCOL = 'wss';\nexport const WS_IP = 'backend-websocket.fsharechat.cn/ws';\nexport const HTTP_IP = 'backend-http.fsharechat.cn';\n//websocket端口,请不要更改\nexport const WS_PORT = 9326;\nexport const HEART_BEAT_INTERVAL = 25 * 1000;\nexport const RECONNECT_INTERVAL = 30 * 1000;\nexport const BINTRAY_TYPE = 'blob';\n\n//signal\nexport const CONNECT = 'CONNECT';\nexport const DISCONNECT = 'DISCONNECT';\nexport const CONNECT_ACK = 'CONNECT_ACK';\nexport const PUBLISH = 'PUBLISH';\nexport const PUB_ACK = 'PUB_ACK';\n//subsignal\nexport const FRP = 'FRP';\nexport const FP = 'FP';\nexport const FALS = 'FALS';\nexport const UPUI = 'UPUI';\nexport const GPGI = 'GPGI';\nexport const GPGM = 'GPGM';\nexport const GAM = 'GAM';\nexport const GC = 'GC';\nexport const GMI = 'GMI';\nexport const GKM = 'GKM';\nexport const GQ = 'GQ';\nexport const GD = 'GD';\nexport const MP = 'MP';\nexport const MS = \"MS\";\nexport const MN = \"MN\";\nexport const MR = \"MR\";\nexport const RMN = \"RMN\";\nexport const GQNUT = \"GQNUT\";\nexport const GMURL = \"GMURL\";\nexport const US = \"US\";\nexport const FAR =  \"FAR\";\nexport const FRN = \"FRN\";\nexport const FHR = \"FHR\";\nexport const FN = \"FN\";\nexport const MMI = \"MMI\";\nexport const LRM = \"LRM\";\n\nexport const HTTP_HOST = \"https://\"+HTTP_IP + \"/\"\nexport const LOGIN_API = HTTP_HOST + \"login\";\nexport const SNED_VERIFY_CODE_API = HTTP_HOST + \"send_code\";;\n\nexport const KEY_VUE_DEVICE_ID = 'vue-device-id';\nexport const KEY_VUE_USER_ID = 'vue-user-id'; \nexport const KEY_VUE_TOKEN = 'vue-token';\n\n//userId 这里为了演示静态登录，由于还没有登录界面所以暂时使用静态userid\nexport const USER_ID = 'TYTzTz33';\nexport const CLINET_ID = 'bccdb58cfdb34d861576810441000';\n//token\nexport const TOKEN = '6Yz2rQDrtRPRc3j9PesLy0De17uX2RlVcvkxU/UmGEaMamd/kaagwWNThIWSGMd6SPVHxLeynho03sJWdbm7wFMRO8VTKf5Wogv7l7gKLsq81mswRha3j6FMdDVHVJ+MLJrVjrThkqXrK1rHwsZvGxpqSGcekHIggI1UEEJSXyQ=';\n\n//是否使用七牛上传文件\nexport const UPLOAD_BY_QINIU = false;\n\nexport const ERROR_CODE = 400;\nexport const SUCCESS_CODE = 200;\n\n//conversation\nexport const CONVERSATION_MAX_MESSAGE_SIZE = 50;\n"
  },
  {
    "path": "src/main.js",
    "content": "// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base.conf with an alias.\nimport Vue from 'vue'\nimport App from './App'\nimport router from './router'\nimport store from './store'\nimport './permission' // permission control\nVue.config.productionTip = false\n\nimport ElementUI from 'element-ui';\nimport 'element-ui/lib/theme-chalk/index.css';\nVue.use(ElementUI);\n/* eslint-disable no-new */\nconst vm = new Vue({\n  el: '#app',\n  router,\n  store,\n  template: '<App/>',\n  components: { App }\n})\n"
  },
  {
    "path": "src/page/chat/chat.vue",
    "content": "<template>\n\t<div class=\"content\">\n\t\t<div class=\"msglist\">\n\t\t\t<search></search>\n\t\t\t<chatlist></chatlist>\n\t\t</div>\n\t\t<div class=\"chatbox\">\n\t\t\t<message></message>\n\t\t\t<v-text></v-text>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport search from '../../components/search/search'\nimport chatlist from '../../components/chatlist/chatlist'\nimport message from '../../components/message/message'\nimport vText from '../../components/text/text'\nimport VueSocket from '../../websocket/index'\nimport {WS_PROTOCOL,WS_IP,WS_PORT,HEART_BEAT_INTERVAL,RECONNECT_INTERVAL,BINTRAY_TYPE} from '../../constant/index'\nexport default {\n   components: {\n   \t search,\n   \t chatlist,\n   \t message,\n   \t vText\n   },\n   mounted(){\n    //   var socket = new VueSocket(WS_PROTOCOL,WS_IP,WS_PORT, HEART_BEAT_INTERVAL, RECONNECT_INTERVAL,BINTRAY_TYPE,this.$store);\n\t//   socket.connect(true);\n   }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.content\n  display: flex\n  width: 100%\n  height: 100%\n  .msglist\n    width: 250px\n    background: #fff\n  .chatbox\n    flex: 1\n</style>\n"
  },
  {
    "path": "src/page/friend/friend.vue",
    "content": "<template>\n\t<div class=\"content\">\n\t\t<div class=\"friend-wrapper\">\n\t\t\t<search></search>\n      <friendlist></friendlist>\n\t\t</div>\n\t\t<div class=\"friendinfo\">\n\t\t\t<info></info>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport search from '../../components/search/search'\nimport friendlist from '../../components/friendlist/friendlist'\nimport info from '../../components/info/info'\nexport default{\n    components: {\n        search,\n        friendlist,\n        info\n    }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.content\n  display: flex\n  width: 100%\n  height: 100%\n  .friend-wrapper\n    width: 250px\n    background: #fff\n  .friendinfo\n    flex: 1\n</style>"
  },
  {
    "path": "src/page/friend/searchfriend.vue",
    "content": "<template>\n    <div class=\"addfrind\">\n        <el-dialog\n        title=\"添加好友\"\n        :visible.sync=\"showSearchFriendDialog\"\n        width=\"30%\"\n        :show-close=\"true\"\n        @close=\"handleClose\"\n        center>\n\n        <el-dialog\n          width=\"30%\"\n          title=\"添加好友\"\n          :visible.sync=\"innerVisible\"\n          append-to-body>\n          <el-input  \n             type=\"textarea\"\n             :rows=\"3\"\n             v-model=\"friendRequest\"></el-input>\n          <span slot=\"footer\" class=\"dialog-footer\">\n            <el-button size=\"medium\" type=\"info\" plain round @click=\"innerVisible=false\">取 消</el-button>\n            <el-button size=\"medium\" type=\"success\" plain round @click=\"sendFriendRequest\">确 定</el-button>\n        </span>   \n        </el-dialog>\n\n        <el-input v-model=\"friendInput\" prefix-icon=\"el-icon-search\" placeholder=\"请输入手机号码或昵称\" @keydown.enter.native=\"searchUser\"></el-input>\n        <el-table\n            :data=\"searchUsers\"\n            :show-header=\"false\"\n            :fit=\"true\"\n            :highlight-current-row=\"false\"\n            max-height=\"300\"\n            style=\"width: 100%;margin-top:10px\">\n            <el-table-column\n                prop=\"image\"\n                label=\"头像\"\n                width=\"50\"\n                >\n                <template slot-scope=\"scope\">\n                    <el-avatar :src=\"scope.row.portrait\"></el-avatar>\n                </template>\n            </el-table-column>\n            <el-table-column\n                prop=\"name\"\n                label=\"姓名\"\n                width=\"120\">\n                <template slot-scope=\"scope\">\n                    <div>\n                        <p>{{scope.row.displayName}}</p>\n                        <p>{{scope.row.mobile}}</p>\n                    </div>\n                </template>\n            </el-table-column>\n            <el-table-column\n                label=\"加好友\"\n                align=\"right\">\n                <template slot-scope=\"scope\">\n                    <i class=\"icon iconfont icon-jiahaoyou\" @click=\"showFriendRequestDialog(scope.row)\"></i>\n                </template>\n            </el-table-column>\n        </el-table>\n        <!-- <span slot=\"footer\" class=\"dialog-footer\">\n            <el-button class=\"search-friend-btn\" size=\"medium\" type=\"info\" plain round @click=\"changeSearchFriendDialog\">取 消</el-button>\n            <el-button class=\"search-friend-btn\"  size=\"medium\" type=\"success\" plain round @click=\"changeSearchFriendDialog\">确 定</el-button>\n        </span> -->\n        </el-dialog>\n    </div>\n</template>\n\n<script>\nimport { mapGetters, mapState } from 'vuex'\nexport default {\n    data() {\n        return {\n            friendInput: '',\n            friendRequest: '',\n            currentSearchUser: null,\n            innerVisible: false\n        }\n    },\n    methods: {\n      changeSearchFriendDialog(){\n        this.$store.state.showSearchFriendDialog = false;\n      },\n      searchUser(e){\n         console.log(\"search user \");\n         if(e.keyCode === 13 && this.friendInput != \"\"){\n            this.$store.dispatch('searchUser', this.friendInput);\n            this.friendInput = '';\n         }\n      },\n      handleClose(){\n          this.$store.dispatch('updateSearchUser', []);\n      },\n      sendFriendRequest(){\n          this.innerVisible=false;\n          this.$store.dispatch(\"sendFriendAddRequest\",{\n            reason: this.friendRequest,\n            targetUserId: this.currentSearchUser.uid\n        });\n      },\n      showFriendRequestDialog(currentSearchUser){\n        this.innerVisible=true;\n        this.friendRequest = \"我是\"+this.$store.state.user.name;\n        this.currentSearchUser = currentSearchUser;\n      }\n   },\n   computed: {\n     ...mapState([\n          'searchUsers',\n        ]),\n     showSearchFriendDialog : {\n         get () {\n             return this.$store.state.showSearchFriendDialog;\n         },\n         set(val) {\n             this.$store.state.showSearchFriendDialog = val;\n         }\n     }\n   }\n}\n</script>\n\n<style scoped>\n.addfrind .icon {\n   display: inline-block;\n   font-size: 26px; \n   color: rgb(0,220,65);\n}\n.layout-container {\n    width: 100%;\n    height: 100%;\n    box-sizing: border-box;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column\n}\n\n.el-dialog__wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.search-friend-btn {\n   width: 50%;\n   height: 35px;\n}\n\n</style>"
  },
  {
    "path": "src/page/group/creategroup.vue",
    "content": "<template>\n   <div class=\"create-group\" v-if=\"showCreateGroupDialog\">\n       <el-dialog\n        :visible.sync=\"showCreateGroupDialog\"\n        width=\"45%\"\n        :show-close=\"false\"\n        @close=\"handleClose\"\n        :distroy-on-close=\"true\"\n        center>\n            <div class=\"content\" >\n                <div class=\"friendlist-area\">\n                    <div class=\"search-input\">\n                        <el-input clearable v-model=\"friendInput\" prefix-icon=\"el-icon-search\" placeholder=\"搜索\" @keydown.enter.native=\"searchUser\"></el-input>\n                    </div>\n                    <div class=\"friendlist\" :style=\"{height: (appHeight-170) + 'px'}\">\n                        <ul>\n                            <li v-bind:key = index v-for=\"(item, index) in waitCheckedFriendList\" class=\"frienditem\"  :class=\"{ noborder: !item.initial}\">\n                                <div class=\"list_title\" v-if=\"item.initial\">{{item.initial}}</div>\n                                <div class=\"friend-info\" :class=\"{ active: item.id === selectFriendId && !item.disabled,disable: item.disabled }\" @click.stop=\"selectFriend(item.id)\">\n                                    <img class=\"avatar\" :src=\"item.img\" onerror=\"this.src='static/images/vue.jpg'\">\n                                    <div class=\"remark\">{{item.remark}}</div>\n                                    <div class=\"friend-check\">\n                                        <el-checkbox :true-label=\"item.id+':1'\" :false-label=\"item.id+':0'\" @change=\"friendChangeChange\"  @click.stop.native=\"\" v-model=\"item.checked\" :disabled=\"item.disabled\"></el-checkbox>\n                                    </div>\n                                </div>\n                                \n                            </li>\n                        </ul>\n                    </div>\n                    \n                </div>\n                <div class=\"checkedlist\">\n                    <div class=\"checked-title\">\n                        <div class=\"create-group-title\">{{groupDialogTitle}}</div>\n                        <div class=\"check-statu-title\">{{checkFriendTips}}</div>\n                    </div>\n                    <div class=\"friendlist\" :style=\"{height: (appHeight-200) + 'px'}\">\n                        <ul>\n                            <li v-bind:key = index v-for=\"(item, index) in selectedFriends\" class=\"frienditem\">\n                                <div class=\"friend-info\" >\n                                    <img class=\"avatar\" :src=\"item.img\" onerror=\"this.src='static/images/vue.jpg'\">\n                                    <div class=\"remark\">{{item.remark}}</div>\n                                    <div class=\"delete\" @click=\"delfriend(item.id)\"></div>\n                                </div>\n                            </li>\n                        </ul>\n                    </div>\n                    <div class=\"check-operate\">\n                        <div class=\"check-btns\">\n                            <el-button class=\"cancel-btn\" size=\"medium\" type=\"info\" plain round @click=\"cancel\">取 消</el-button>\n                            <el-button class=\"confirm-btn\"  size=\"medium\" type=\"success\" plain round \n                                @click=\"confirm\" \n                                :disabled=\"confirmEnable\" \n                                v-loading.fullscreen.lock=\"fullscreenLoading\">确 定</el-button>\n                        </div>\n                        \n                    </div>\n                </div>\n            </div>\n       </el-dialog>\n   </div>\n    \n</template>\n\n<script>\nimport { mapGetters, mapState } from 'vuex'\nimport Logger from '../../websocket/utils/logger'\nimport webSocketClient  from '../../websocket/websocketcli'\nimport LocalStore from '../../websocket/store/localstore'\nimport { SUCCESS_CODE } from '../../constant'\nexport default {\n    name: 'creategroup',\n    data() {\n       return {\n           friendInput: '',\n           selectedFriends: [],\n           checkFriendTips: '未选择联系人',\n           confirmEnable: true,\n           selectFriendId: 0,\n           fullscreenLoading: false,\n       }\n    },\n    computed: {\n        ...mapState([\n            'appHeight',\n            'user',\n            'groupOperateState',\n            'groupMemberMap',\n            'selectTarget',\n            'groupMemberTracker'\n        ]),\n        ...mapGetters([\n            'searchedFriendlist',\n        ]),\n        showCreateGroupDialog : {\n            get () {\n                return this.$store.state.showCreateGroupDialog;\n            },\n            set(val) {\n                this.$store.state.showCreateGroupDialog = val;\n            }\n        },\n        waitCheckedFriendList(){\n            let friends = this.searchedFriendlist.slice(1,this.searchedFriendlist.length);\n            var listunCheckedFriends = [];\n            for(var friend of friends){\n                var isChecked = false;\n                var isDisabled = false;\n                var isShow = false\n                if(this.groupOperateState == 0){\n                   isShow = true;\n                } else if(this.groupOperateState == 1){\n                    //only for update groupMember ,只能监听map类型，不能监听getter\n                    var trackTime = this.groupMemberTracker;\n                    isShow = true;\n                    var currentMember = this.groupMemberMap.get(this.selectTarget).find( member => member.memberId == friend.wxid)\n                    if(currentMember){\n                        isChecked = true;\n                        isDisabled = true;\n                    }\n                } else if(this.groupOperateState == 2){\n                    var trackTime = this.groupMemberTracker;\n                    var currentMember = this.groupMemberMap.get(this.selectTarget).find( member => member.memberId == friend.wxid)\n                    if(currentMember){\n                        isChecked = false;\n                        isShow = true;\n                    }\n                } else if(this.groupOperateState == 3){\n                    var trackTime = this.groupMemberTracker;\n                    isShow = true;\n                    if(this.selectTarget == friend.wxid){\n                        isChecked = true;\n                        isDisabled = true;\n                    }\n                } else if(this.groupOperateState == 4){\n                    var trackTime = this.groupMemberTracker;\n                    var groupMembers = this.groupMemberMap.get(this.selectTarget);\n                    if(groupMembers){\n                        var currentMember = groupMembers.find( member => member.memberId == friend.wxid)\n                        if(currentMember){\n                            isChecked = false;\n                            isShow = true;\n                        }\n                    } else {\n                        webSocketClient.getGroupMember(this.selectTarget).then(data => {\n                            if(data.code == SUCCESS_CODE){\n                                this.groupMemberMap.set(this.selectTarget,data.result);\n                                this.$store.state.groupMemberTracker += 1;\n                            }\n                        })\n                    }\n                    \n                }\n                Logger.log(\"friend nickname \"+friend.remark+\" ischecked \"+isChecked)\n                if(isShow){\n                    listunCheckedFriends.push({\n                        id: friend.id,\n                        wxid: friend.wxid,\n                        remark: friend.remark,\n                        img: friend.img,\n                        initial: friend.initial,\n                        checked: isChecked,\n                        disabled: isDisabled\n                    });\n                }\n                \n            }\n            return listunCheckedFriends;\n        },\n        groupDialogTitle(){\n            if(this.groupOperateState == 0){\n                return '创建群聊'\n            } else if(this.groupOperateState == 1){\n                return '添加成员'\n            } else if(this.groupOperateState == 2){\n                return '移除成员'\n            } else if(this.groupOperateState == 3){\n                return '加入群聊'\n            } else if(this.groupOperateState == 4){\n                return '选择群组音视频成员'\n            }\n        }\n   },\n   methods:{\n       friendChangeChange(event) {\n           Logger.log(\"check change \"+event);\n           if(event && event != \"\"){\n                var friendIdAndChecked = event.split(\":\");\n                Logger.log(\"select friendid \"+friendIdAndChecked[0]+\" checked \"+friendIdAndChecked[1]);\n                var friend = this.waitCheckedFriendList.find(friend => friend.id == friendIdAndChecked[0]);\n                Logger.log(\"friend change \"+friend.checked);\n                if(friendIdAndChecked[1] == 1){\n                    if(friend){\n                        this.selectFriendId = friend.id;\n                        var existFriend = this.selectedFriends.find(friend => friend.id == this.selectFriendId);\n                        if(!existFriend){\n                            this.selectedFriends.push(friend);\n                        }\n                    }\n                } else if(friendIdAndChecked[1] == 0){\n                    if(friend){\n                        this.removeCheckedFriend(friend.id);\n                    }\n                }    \n           }\n       },\n       selectFriend(friendId){\n           this.selectFriendId = friendId;\n           var friend = this.waitCheckedFriendList.find(friend => friend.id == friendId);\n        //    Logger.log(\"selectFriend friendId \"+friendId+\" checked \"+friend.checked+\" disabled \"+friend.disabled);\n           if(friend.disabled){\n                return\n           }\n           friend.checked = !friend.checked;\n           if(!friend.checked){\n               this.removeCheckedFriend(friend.id);\n           } else {\n               this.selectedFriends.push(friend);\n           }\n       },\n       cancel(){ \n            this.exit();\n       },\n       confirm(){\n           if(this.selectedFriends.length > 0){\n                this.fullscreenLoading = true;\n                var groupName = this.user.name;\n                var memberIds = [];\n                for(var index in this.selectedFriends){\n                    if (index < 2){\n                        groupName += \"、\"+this.selectedFriends[index].remark;\n                    }\n                    memberIds.push(this.selectedFriends[index].wxid);\n                }\n            \n                switch (this.groupOperateState) {\n                    case 0:\n                        //将自己加入到群组中\n                        memberIds.push(LocalStore.getUserId());\n                        webSocketClient.createGroup(groupName,memberIds).then(data => {\n                            Logger.log(\"create group result \"+data);\n                            if(data.code == SUCCESS_CODE){\n                                var result = JSON.parse(data.result);\n                                this.fullscreenLoading = false;\n                                this.exit();\n                                this.$message({\n                                    type: 'success',\n                                    message: '创建'+groupName+'群组成功!'\n                                 });\n                            } else {\n                                this.fullscreenLoading = false;\n                                this.$message({\n                                    type: 'success',\n                                    message: '创建'+groupName+'群组失败,请重试!'\n                                 });\n                            }\n                        });\n                        break\n                    case 1:\n                        webSocketClient.addMembers(this.selectTarget,memberIds).then(data => {\n                            Logger.log(\"add member result \"+data)\n                            if(data.code == SUCCESS_CODE){\n                                this.fullscreenLoading = false;\n                                this.exit();\n                            } else {\n                                this.fullscreenLoading = false;\n                            }\n                        })\n                        break\n                    case 2:\n                        webSocketClient.kickeMembers(this.selectTarget,memberIds).then(data => {\n                            if(data.code == SUCCESS_CODE){\n                                this.fullscreenLoading = false;\n                                this.exit();\n                            } else {\n                                this.fullscreenLoading = false;\n                            }\n                        })\n                        break\n                    case 3:\n                       //增加当前用户，与现在聊天的用户\n                       memberIds.push(LocalStore.getUserId());\n                       memberIds.push(this.selectTarget);\n                       webSocketClient.createGroup(groupName,memberIds).then(data => {\n                           if(data.code == SUCCESS_CODE){\n                                var result = JSON.parse(data.result);\n                                this.fullscreenLoading = false;\n                                this.exit();\n                            } else {\n                                this.fullscreenLoading = false;\n                            }\n                       })\n                    case 4:\n                        this.$store.state.showGroupCallVideoDialog = true\n                        this.$store.state.groupCallMembers = memberIds\n                        console.log(\"group call members \"+memberIds)\n                        this.fullscreenLoading = false;\n                        this.exit();\n                        break;                \n                    default:\n                        break\n\n                }\n\n           }\n           \n       },\n       handleClose(){\n            this.exit();\n       },\n       delfriend(id){\n          var friend = this.waitCheckedFriendList.find(friend => friend.id == id);\n          friend.checked = false;\n          this.removeCheckedFriend(id);\n       },\n       removeCheckedFriend(id){\n            var index = -1;\n            for(var i=0;i<this.selectedFriends.length;i++){\n                if(id == this.selectedFriends[i].id){\n                    index = i;\n                    break;\n                }\n            }\n            if(index != -1){\n                this.selectedFriends.splice(index,1);\n            }\n            if(id == this.selectFriendId){\n               this.selectFriendId = 0;\n            }\n       },\n       exit(){\n           this.$store.state.showCreateGroupDialog = false;\n           this.selectedFriends = [];\n           for(var friend of this.waitCheckedFriendList){\n                if(!friend.disabled){\n                    friend.checked = false;\n                }\n           }           \n       },\n   },\n   watch: {\n       selectedFriends() {\n           if(this.selectedFriends.length == 0){\n              this.selectFriendId = 0;\n           }\n           if(this.selectedFriends.length > 0){\n              if(this.groupOperateState == 0 ){\n                 if(this.selectedFriends.length > 1){\n                     this.confirmEnable = false;\n                 } else {\n                     this.confirmEnable = true;\n                 }\n              } else {\n                 this.confirmEnable = false;\n              }\n           } else {\n               this.confirmEnable = true;\n           }\n           if(this.selectedFriends.length == 0){\n               this.checkFriendTips = \"未选择联系人\";\n           } else {\n               this.checkFriendTips = \"已选择\"+this.selectedFriends.length+\"位联系人\";\n           }\n       }\n   }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.content\n  display: flex\n  width: 100%\n  height: 50%\n  .friendlist-area\n    width: 50%\n    padding: 2px 0px 0px 0px\n    background: #fff\n    border-right: 1px solid #e7e7e7\n    .search-input\n        margin-right: 10px\n    .friendlist\n        overflow-y: auto\n        .frienditem\n            border-top: 1px solid #dadada\n            &:first-child,&.noborder\n                border-top: none\n            .list_title\n                box-sizing: border-box\n                width: 100%\n                font-size: 12px\n                padding: 15px 0 3px 12px\n                color: #999\n            .friend-info\n                display: flex\n                padding: 5px\n                transition: background-color .1s\n                font-size: 0\n                &:hover \n                    background-color: rgb(220,220,220)\n                &.active \n                    background-color: #c4c4c4 \n                .disable\n                    pointer-events: none;     \n                .avatar\n                    border-radius: 2px\n                    margin-right: 12px\n                    width: 35px\n                    height: 35px\n                .remark\n                    font-size: 14px\n                    line-height: 26px\n                    display: flex\n                    align-items: center \n                .friend-check\n                    margin-left : auto \n                    display: flex\n                    padding-right: 5px\n                    align-items: center\n  .checkedlist\n    display: flex\n    flex-flow: row wrap\n    padding: 2px 0px 2px 10px\n    width: 50%\n    .checked-title\n        display: flex\n        padding: 5px 0px 10px 0px\n        width: 100%\n        height: 30px\n        .create-group-title\n            font-size: 14px\n            color: black \n        .check-statu-title\n            font-size: 10px\n            color: #999\n            line-height: 15px\n            margin-left : auto\n    .check-operate\n        width: 100%\n        align-self: flex-end\n        .check-btns\n            width: 164px\n            margin-left : auto \n            display: flex\n    .friendlist\n        overflow-y: auto\n        width: 100%\n        .frienditem\n            padding: 5px 0px 5px 0px\n            .friend-info\n                display: flex\n                padding: 5px\n                .avatar\n                    border-radius: 2px\n                    margin-right: 12px\n                    width: 35px\n                    height: 35px\n                .delete\n                    margin-left : auto \n                    outline: none\n                    width: 35px\n                    height: 35px\n                    background-size: 26px\n                    background-position: center\n                    background-repeat: no-repeat\n                    background-image: url(delete.png)\n                    cursor: pointer\n                .remark\n                    font-size: 14px\n                    line-height: 26px\n                    color: black\n                    display: flex\n                    align-items: center\n\n.el-dialog__wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n</style>"
  },
  {
    "path": "src/page/group/groupVideoCall.vue",
    "content": "<template>\n    <div class=\"callContent\" v-if=\"showGroupCallVideoDialog\">\n        <div class=\"callercontent callshow\" style=\"\">\n            <div class=\"exchange-content\">\n                <div class=\"playcontent left-big-content\">\n                    <div class=\"remote-video\"  v-bind:key = index v-for=\"(item, index) in currentGroupCallMembers\">\n                        <img class=\"bigavatar\" :src=\"item.img\" v-show=\"!item.showRemoteVideo\"/> \n                        <p class=\"calltips\" v-text=\"videoTextCallTips\" v-show=\"showCallTips\"> 接通中... </p> \n                        <video :id=\"item.id\" autoplay=\"autoplay\" playsinline=\"\" style=\"display: none;\" v-show=\"item.showRemoteVideo\"></video>\n                    </div>\n                    \n                </div> \n                <div class=\"playcontent right-sml-content\">\n                    <img id = \"wxCallLocalImg\" :src=\"callLocalImg\" class=\"bigavatar\" v-show=\"showCallLocalImg\"/> \n                    <video :id=\"userId\" autoplay=\"autoplay\" muted=\"muted\" playsinline=\"\" style=\"display: none;\" v-show=\"showCallLocalVideo\"></video>\n                </div>\n            </div> \n            <div class=\"opera-content flexbox\">\n                <img class=\"calleravatar\" :src=\"callRemoteImg\" /> \n                <span class=\"flexauto overell callnick\" v-text=\"callDisplayName\"></span> \n                <span class=\"flexbox\">\n                    <span class=\"operabtn canclecall btnopacity\" v-show=\"cancelCall\" @click=\"cancel\">取消</span> \n                    <span class=\"operabtn canclecall btnopacity\" style=\"display: none;\" v-show=\"rejectCall\" @click=\"reject\">拒绝</span> \n                    <span class=\"operabtn upcall btnopacity\" style=\"display: none;\" v-show=\"acceptCall\" @click=\"accept\">接听</span>\n                </span> \n                <span class=\"talktime flexbox\" style=\"display: none;\" v-show=\"showTalkTime\"><i class=\"iconfont icon-ai-video\"></i> <span v-text=\"talkTime\">00:00</span></span> \n                <span class=\"operabtn canclecall btnopacity\" style=\"display: none;\" v-show=\"hangUpCall\" @click=\"hangUp\"><i class=\"iconfont icon-guaduan\"></i>挂断 </span> \n                <button class=\"screenbtn\"><i class=\"iconfont icon-quanping iconfull\" style=\"display: none;\"></i></button> \n                <button class=\"screenbtn\" style=\"display: none;\"><i class=\"iconfont icon-tuichuquanping iconfull\"></i></button>\n            </div>\n        </div>\n    </div>\n    \n</template>\n\n<script>\nimport { mapGetters, mapState } from 'vuex'\nimport SessionCallback from '../../webrtc/sessionCallback';\nimport EngineCallback from '../../webrtc/engineCallback';\nexport default {\n    data(){\n        return {\n            currentGroupTarget: '',\n            groupCallClient: null,\n            showCallLocalImg: true,\n            showCallLocalVideo: false,\n            showCallRemoteImg: true,\n            showCallRemoteVideo: false,\n            showCallTips: true,\n            cancelCall: false,\n            rejectCall: false,\n            hangUpCall: false,\n            acceptCall: false,\n            callRemoteImg: 'static/images/vue.jpg',\n            callLocalImg: 'static/images/vue.jpg',\n            videoTextCallTips: '',\n            callDisplayName: '',\n            talkTime: '00:00',\n            showTalkTime: false,\n            isAudioOnly: true,\n            isSender: true,\n            groupMemberInfos: []\n        }\n    },\n    mounted(){\n        console.log(\"group call mounted\")\n        var sessionCallback = new SessionCallback();\n        sessionCallback.didError = error =>{\n            this.$message.error('请确认你的设备具有音视频设备');\n            this.cancel()\n            this.groupMemberInfos = []\n        }\n        sessionCallback.didCreateLocalVideoTrack = stream => {\n            console.log(\"didCreateLocalVideoTrack\")\n            this.showCallLocalImg = false;\n            this.showCallLocalVideo = true;\n        }\n\n        sessionCallback.didCallEndWithReason = (callEndReason,sender) => {\n           if(this.userId == sender){\n              this.isSender = true  \n              this.showGroupCallVideoDialog = false\n              this.groupMemberInfos = []\n           } else {\n               var user = this.currentGroupCallMembers.find(user => user.id == sender)\n               if(user){\n                user.showRemoteVideo = false\n               }\n           }\n        }\n\n        sessionCallback.didReceiveRemoteVideoTrack = (stream,sender) => {\n           var user = this.currentGroupCallMembers.find(user => user.id == sender)\n           console.log(\"didReceiveRemoteVideoTrack user \"+user.id+\" sender \"+sender)\n           if(user){\n              user.showRemoteVideo = true\n           }\n        }\n\n\n        var engineCallback = new EngineCallback()\n        engineCallback.onReceiveCall = session => {\n            this.$store.state.groupCallMembers = session.tos\n            this.isSender = false\n            this.showGroupCallVideoDialog = true\n            this.rejectCall = true;\n            this.cancelCall = false;\n            this.acceptCall = true;\n            this.hangUpCall = false;\n            this.videoTextCallTips = '';\n            this.showCallLocalImg = true;\n            this.showCallLocalVideo = false;\n            this.showCallTips = true;\n            this.initGroupInfo(session.clientId);\n            this.initCallUserInfo()\n        }\n\n\n        engineCallback.shouldStartRing = isIncomming => {\n            if(isIncomming){\n                this.inCommingNotify.loopPlay()\n            } else {\n                this.outGoingNotify.loopPlay();\n            }\n        }\n\n        engineCallback.shouldSopRing = () => {\n            this.inCommingNotify.stopPlay();\n            this.outGoingNotify.stopPlay();\n        }\n\n        this.groupCallClient = this.$store.state.groupCallClient\n        this.groupCallClient.setCurrentSessionCallback(sessionCallback)\n        this.groupCallClient.setCurrentEngineCallback(engineCallback)\n    },\n    methods:{\n        startVideoCall(){\n            this.cancelCall = true\n            this.rejectCall = false;\n            this.acceptCall = false;\n            this.hangUpCall = false;\n            this.isAudioOnly = false\n            this.initGroupInfo(this.selectTarget);\n            this.initCallUserInfo()\n            this.groupCallClient.startCall(this.selectTarget,this.groupCallMembers,this.isAudioOnly)\n        },\n        cancel(){\n            this.cancelCall = false\n            this.showGroupCallVideoDialog = false\n            this.groupMemberInfos = []\n            this.groupCallClient.endCall(this.groupCallMembers)\n        },\n        hangUp(){\n            this.groupCallClient.endCall()\n            this.groupMemberInfos = []\n        },\n        reject(){\n           this.groupCallClient.endCall()\n           this.groupMemberInfos = []\n        },\n        accept(){\n           this.rejectCall = false;\n           this.acceptCall = false;\n           this.hangUpCall = true;\n           this.groupCallClient.answerCall(false);\n        },\n        initGroupInfo(target){\n           this.currentGroupTarget = target\n           var groupInfo = this.groupInfoList.find(groupInfo => groupInfo.target == target)\n           console.log(\"group info \"+groupInfo)\n           if(groupInfo && groupInfo.portrait != ''){\n              this.callRemoteImg = groupInfo.portrait\n              this.callDisplayName = groupInfo.name\n           } else {\n               this.$store.dispatch(\"getGroupInfo\",target);\n           }\n        },\n        initCallUserInfo(){\n            this.callLocalImg = this.$store.state.user.img;\n        },\n        getUserPortrait(target){\n            var userInfo = this.userInfoList.find(userInfo => userInfo.uid == target);\n            if(userInfo){\n                return userInfo.portrait;\n            }\n            return null;\n        },\n        getDisplayName(target){\n            var userInfo = this.userInfoList.find(userInfo => userInfo.uid == target);\n            if(userInfo){\n                return userInfo.displayName;\n            } else {\n                return ''\n            }\n        },\n    },\n    computed:{\n        ...mapState([\n            'groupCallMembers',\n            'userInfoList',\n            'selectTarget',\n            'userId',\n            'groupInfoList',\n            'inCommingNotify',\n            'outGoingNotify'\n        ]),\n        currentGroupCallMembers(){\n            if(this.groupMemberInfos.length == 0){\n                this.groupCallMembers.forEach(memberId => {\n                    var user = this.userInfoList.find(user => user.uid == memberId)\n                    this.groupMemberInfos.push({\n                        id: user.uid,\n                        name: user.displayName,\n                        img: user.portrait != '' ? user.portrait : 'static/images/vue.jpg',\n                        showRemoteVideo: false\n                    })\n               });\n            }\n            return this.groupMemberInfos;\n        },\n        showGroupCallVideoDialog : {\n            get () {\n                return this.$store.state.showGroupCallVideoDialog;\n            },\n            set(val) {\n                this.$store.state.showGroupCallVideoDialog = val;\n            }\n        },\n    },\n\n    watch: {\n        showGroupCallVideoDialog(){\n            if(this.showGroupCallVideoDialog && this.isSender){\n               this.startVideoCall()\n            }\n        },\n        groupInfoList(){\n           var groupInfo = this.groupInfoList.find(groupInfo => groupInfo.target == this.currentGroupTarget)\n           if(groupInfo){\n               if(groupInfo.portrait != ''){\n                this.callRemoteImg = groupInfo.portrait\n               }\n              this.callDisplayName = groupInfo.name\n           }\n        }\n    }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.callContent\n    position: fixed\n    top: 0\n    right: 0\n    bottom: 0\n    left: 0\n    overflow: auto\n    margin: 0;\n    z-index: 2000\n    .callercontent\n        width: 634px;\n        height: 414px;\n        position: absolute;\n        left: 0;\n        right: 0;\n        margin: auto;\n        top: 0;\n        bottom: 0;\n        z-index: 2000\n\n    .callercontent video\n        width: 160px;\n        height: 120px;\n        background: #000\n\n    .callercontent.callnone\n        display: none\n    .callercontent.callshow\n        display: block\n    .left-big-content\n        width: 659px;\n        height: 360px;\n        position: absolute;\n        background: #fff;\n        left: 0;\n        top: 0\n    .left-big-content \n        .remote-video\n            display inline-block   \n    .left-big-content .bigavatar\n        width: 160px;\n        height: 120px;\n        filter: blur(0px)\n\n    .left-big-content video\n        width: 160px;\n        height: 120px;\n        background: #000\n    .right-sml-content\n        width: 160px;\n        height: 120px;\n        box-shadow: 0 6px 20px 0 rgba(48,52,58,0.5);\n        border-radius: 4px;\n        position: absolute;\n        right: -25px;\n        bottom: 54px\n    .right-sml-content .bigavatar\n        width: 100%;\n        height: 100%;\n        border-radius: 4px\n    .opera-content\n        padding: 10px 16px;\n        box-shadow: 0 6px 20px 0 rgba(48,52,58,0.5);\n        height: 56px;\n        width: 659px;\n        background: #fff;\n        position: absolute;\n        bottom: 0;\n        left: 0\n    .opera-content .calleravatar\n        width: 36px;\n        height: 36px;\n        margin-right: 16px;\n        flex-shrink: 0;\n        border-radius: 100%\n    .opera-content .callnick\n        color: #30343a\n    .opera-content .operabtn\n        width: 72px;\n        height: 32px;\n        border-radius: 6px;\n        color: #fff;\n        text-align: center;\n        font-size: 12px;\n        line-height: 32px;\n        cursor: pointer\n    .opera-content .canclecall\n        background: #ff6161\n    .opera-content .canclecall .iconfont\n        color: #fff;\n        font-size: 16px;\n        margin-right: 8px\n    .opera-content .upcall\n        margin-left: 16px;\n        background: #39ba70\n    .calltips\n        position: absolute;\n        margin: auto;\n        text-align: center;\n        color: #fff;\n        font-size: 16px;\n        z-index: 10;\n        left: 0;\n        right: 0;\n        top: 0;\n        line-height: 360px\n    .flexshrink\n        flex-shrink: 0\n    .iconfull\n        margin-left: 16px;\n        font-size: 16px;\n        cursor: pointer\n    .screenbtn\n        background: #fff;\n        border: 0\n    .talktime span\n        font-size: 12px;\n        color: #30343a;\n        margin-left: 8px;\n        margin-right: 16px\n    .flexbox\n        display: flex;\n        align-items: center\n    .flexauto\n        flex: 1   \n</style>"
  },
  {
    "path": "src/page/login/login.vue",
    "content": "<template>\n  <!--  账号输入登录-->\n  <div class=\"login_box\">\n    <router-link to=\"#\">\n      <div class=\"login_close\"></div>\n    </router-link>\n    <div class=\"login_panel\">\n      <div class=\"login_title\">\n        <img src=\"../../assets/img/logo.png\" alt=\"\">\n        <p>输入账号进行安全登录</p>\n      </div>\n      <label style=\"margin-top: 50px\">手机号码：</label>\n      <input v-model=\"mobile\" type=\"tel\" pattern=\"^\\d{11}$\" title=\"请输入账号\">\n      <label>验证码：</label>\n      <div class=\"pass-form\">\n        <input class=\"pass-input\" v-model=\"code\" type=\"num\" title=\"请输入密码\" @keydown.enter=\"loginEnter\">\n        <el-button class=\"send-verify-code\" type=\"primary\" :disabled=\"sendVerifyBtnDisabled\" @click=\"sendVerifyCode\">{{sendVerifyBtnText}}</el-button>\n      </div>\n      <input class=\"bt\" @click=\"login\" type=\"submit\" value=\"登录\">\n    </div>\n  </div>\n</template>\n\n<script>\nimport { LOGIN_API, KEY_VUE_DEVICE_ID, KEY_VUE_USER_ID, KEY_VUE_TOKEN, SNED_VERIFY_CODE_API } from '../../constant'\nimport { mapGetters } from 'vuex'\nimport UUID from 'uuid-js'\nimport axios from 'axios'\nexport default {\n  name: 'Login',\n  data () {\n    return {\n      mobile: '',\n      code: '',\n      countDownTimer: null,\n      sendVerifyBtnText: '发送验证码',\n      sendVerifyBtnDisabled: false,\n      loginAPI: LOGIN_API, // 通过用户ID登录接口\n    }\n  },\n  destroyed() {\n      if(this.countDownTimer){\n          clearInterval(this.countDownTimer);\n      }\n  },\n  methods: {\n    login () {\n      //初始唯一id\n        let vueDeviceId = localStorage.getItem(KEY_VUE_DEVICE_ID);\n        if(vueDeviceId == null){\n           vueDeviceId = UUID.create().toString();\n           console.log('generate device id '+vueDeviceId);\n           localStorage.setItem(KEY_VUE_DEVICE_ID,vueDeviceId);\n        }\n        console.log('vue deviceId '+vueDeviceId);\n        if(!(/^1[3|4|5|7|8|9]\\d{9}$/.test(this.mobile))){\n           this.$message.error('请输入正确的手机号');\n           return; \n        }\n        axios({\n            method: 'post',\n            url: LOGIN_API,\n            data: JSON.stringify({\n              mobile: this.mobile,\n              code: this.code,\n              clientId: vueDeviceId,\n            }),\n            headers: {\n              'Content-Type': 'application/json;charset=UTF-8'\n            }\n          }).then((response) => {\n            console.log('login code '+response.data.code+\" message \"+response.data.message)\n              if(response.data.code == 0){\n                  var userId = response.data.result.userId;\n                  var token = response.data.result.token;\n                  console.log('userId '+userId+\" token \"+token);\n                  this.$store.state.userId = userId;\n                  this.$store.state.token = token;\n                  localStorage.setItem(KEY_VUE_USER_ID,userId);\n                  localStorage.setItem(KEY_VUE_TOKEN,token);\n                  //跳转到聊天页面\n                  this.$router.push({path: '/conversation'})\n              } else {\n                this.$message.error(response.data.message);\n              }\n          }).catch((error) => {\n            console.log(error)\n          })\n    },\n    loginEnter(e){\n        if(e.keyCode === 13 && this.mobile != '' && this.code != ''){\n            this.login();\n        }\n    },\n    sendVerifyCode(){\n        if(!(/^1[3|4|5|7|8|9]\\d{9}$/.test(this.mobile))){\n           this.$message.error('请输入正确的手机号');\n           return; \n        }\n        axios({\n          method: 'post',\n          url: SNED_VERIFY_CODE_API,\n          data: JSON.stringify({\n            mobile: this.mobile\n          }),\n          headers: {\n            'Content-Type': 'application/json;charset=UTF-8'\n          }\n        }).then(response => {\n          console.log('send code '+response.data.code+\" message \"+response.data.message)\n          if(response.data.code == 0){\n            var _this = this;\n            this.sendVerifyBtnDisabled = true; \n            var countDown = 60;\n            this.countDownTimer = setInterval(() => {\n              _this.sendVerifyBtnText = --countDown +\"S\";\n              // console.log(\"countdown \"+countDown);\n              if(countDown == 0){\n                 clearInterval(_this.countDownTimer);\n                 _this.sendVerifyBtnDisabled = false;\n                 _this.sendVerifyBtnText = \"发送验证码\";\n              }\n            }, (1000));\n          } else {\n            this.$message.error(response.data.message);\n          }\n        }).catch(error => {\n          console.log(error)\n        })\n        \n    }\n  },\n}\n</script>\n\n<style scoped>\n  /*登录框*/\n  .login_box {\n    z-index: 99;\n    position: absolute;\n    width: 380px;\n    height: 540px;\n    top: 50%;\n    left: 50%;\n    margin-left: -190px;\n    margin-top: -270px;\n    border-radius: 6px;\n    background-color: #fff;\n    box-shadow: 0 2px 10px #999;\n  }\n  .login_close {\n    position: absolute;\n    top: 0;\n    right: 0;\n    width: 64px;\n    height: 64px;\n    background: url(../../assets/img/qrcode.png) no-repeat right top;\n    background-size: 100% 100%;\n    border-top-right-radius: 5px;\n    cursor: pointer;\n    z-index: 99;\n  }\n  /*登录*/\n  .login_panel {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 270px;\n    height: 540px;\n    padding: 0 0px;\n    transform: translate(-50%, -50%);\n    /* background: #fff; */\n    border-radius: 6px;\n    overflow: hidden;\n  }\n  .login_panel .login_title {\n    text-align: center;\n  }\n  .login_panel .login_title img {\n    margin-top: 60px;\n    height: 80px;\n    width: 80px;\n    border-radius: 50%;\n    padding: 10px;\n  }\n  .login_panel .login_title p {\n    margin-top: 15px;\n    color: #999999;\n    font-size: 15px;\n  }\n  .login_panel label {\n    display: block;\n    font-size: 12px;\n    line-height: 18px;\n    color: #a9a8a5;\n    margin-top: 10px;\n  }\n  .login_panel input {\n    display: inline;\n    height: 42px;\n    padding: 0 5%;\n    line-height: 42px;\n    font-size: 14px;\n    color: #333333;\n    border-radius: 4px;\n    outline: 0;\n    border: 0;\n    width: 100%;\n    background: #d7e8fc;\n  }\n  /* 按钮 */\n  .login_panel .bt {\n    margin-top: 35px;\n    width: 100%;\n    color: #ffffff;\n    background: #379df6;\n    cursor: pointer;\n  }\n  .login_panel .bt:hover {\n    background-color: #2f86f6;\n  }\n\n  .pass-form {\n    /* position: relative; */\n  }\n  .pass-form .pass-input {\n    display: block;\n    float: left;\n    margin-right: 5px;\n    width: 162px;\n  }\n  .pass-form .send-verify-code {\n    display: block;\n    float: left;\n    width: 103px;\n    height: 42px;\n  }\n</style>"
  },
  {
    "path": "src/page/main.vue",
    "content": "<template>\n  <div id=\"app\" :class=\"{fullscreen: changeFullScreenMode}\" ref=\"appRef\">\n    <div class=\"sidebar\">\n      <mycard></mycard>\n    </div>\n    <div class=\"main\">\n      <router-view></router-view>\n    </div>\n    <groupVideoCall></groupVideoCall>\n  </div>\n</template>\n\n<script>\nimport mycard from '../components/mycard/mycard'\nimport { mapActions, mapState } from 'vuex'\nimport VueWebSocket from '../websocket';\nimport {WS_PROTOCOL,WS_IP,WS_PORT,HEART_BEAT_INTERVAL,RECONNECT_INTERVAL,BINTRAY_TYPE} from '../constant/index'\nimport groupVideoCall from './group/groupVideoCall'\nexport default {\n   components: {\n     mycard,\n     groupVideoCall\n   },\n   created () {\n       this.$store.dispatch('initData');\n   },\n   computed: {\n       ...mapState([\n            'changeFullScreenMode'\n        ])\n   },\n   mounted (){\n      window.onresize = () => {\n        var appHeight = this.$refs.appRef.offsetHeight;\n        this.$store.state.appHeight = appHeight;\n      }\n      //初始化时获取appHeight\n      this.$store.state.appHeight = this.$refs.appRef.offsetHeight;\n      document.addEventListener('visibilitychange', this.handleVisiable);\n\t },\n\t destroyed () {\n      window.onresize = null;\n      document.removeEventListener('visibilitychange', this.handleVisiable)\n   },\n\n   methods: {\n    handleVisiable(e) {\n        this.$store.dispatch('visibilityChange',e.target.visibilityState);\n      }  \n   }\n   \n   \n}\n</script>\n\n<style lang=\"stylus\" scoped>\n\n#app\n  &.fullscreen\n    width: 100%\n    height: 100%\n  display: flex\n  border-radius 50px\n  width: 75%\n  height: 80%\n  background-color: #fff\n  .sidebar\n    width: 60px\n    height: 100%\n    background: #2b2c2f\n  .main\n    flex: 1\n    height: 100%\n    background: #ffffff\n</style>\n"
  },
  {
    "path": "src/permission.js",
    "content": "import router from './router'\nimport store from './store'\nconst whiteList = ['/login'] // 不重定向白名单\nrouter.beforeEach((to, from, next) => {\n  var token = localStorage.getItem('vue-token');\n  console.log('match route token '+token+\" to \"+to.path +\" from \"+from.path +\" next \"+next.path);\n  if (token) {\n    if (to.path === '/login') {\n      next({ path: '/conversation' })\n    } else {\n        next();\n    }\n  } else {\n    if (whiteList.indexOf(to.path) !== -1) {\n      next()\n    } else {\n      next('/login')\n    }\n  }\n})"
  },
  {
    "path": "src/router/index.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\nconst router = new Router({\n  mode: 'history',\n// 共三个页面： 聊天页面，好友页面，个人简历分别对应一下路由\n  routes: [\n    {\n      path: '/login',\n      component: require('@/page/login/login.vue')\n    },\n    {\n      path: '/',\n      component: require('@/page/main.vue'),\n      redirect: 'conversation',\n      children: [{\n          path: 'conversation',\n          name: 'conversation',\n          component: require('@/page/chat/chat.vue'),\n          hidden:true,\n      },\n      {\n        path: 'friend',\n        name: 'friend',\n        component: require('@/page/friend/friend.vue'),\n        hidden:true,\n      }],\n    },\n  ],\n  linkActiveClass: 'active'\n})\n// router.push({ path: '/chat' });\nexport default router"
  },
  {
    "path": "src/store.js",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport router from './router'\nimport VueWebSocket from './websocket';\nimport VoipClient from './webrtc/voipclient'\nimport GroupCallClient from './webrtc/groupCallClient'\nimport {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'\nimport StateConversationInfo from './websocket/model/stateConversationInfo';\nimport StateChatMessage from './websocket/model/stateSelectChatMessage'\nimport Message from './websocket/message/message';\nimport ProtoMessage from './websocket/message/protomessage';\nimport ConversationType from './websocket/model/conversationType';\nimport LocalStore from './websocket/store/localstore';\nimport ProtoConversationInfo from './websocket/model/protoConversationInfo';\nimport UnreadCount from './websocket/model/unReadCount';\nimport StateSelectChateMessage from './websocket/model/stateSelectChatMessage';\nimport Notify from '@wcjiang/notify';\nimport MessageConfig from './websocket/message/messageConfig';\nimport ChatManager from './websocket/chatManager';\nimport ProtoMessageContent from './websocket/message/protomessageContent';\nimport Logger from './websocket/utils/logger';\nimport RecallMessageNotification from './websocket/message/notification/recallMessageNotification';\nimport MessageStatus from './websocket/message/messageStatus';\n\nVue.use(Vuex)\n\n//获取当前时间\nconst now = new Date();\nconst state = {\n\t// 输入的搜索值\n\tsearchText: '',\n\t// 当前登录用户\n    user: {\n    \tname: 'ratel',\n    \timg: 'static/images/vue.jpg'\n    },\n    // 好友列表\n    friendlist: [\n        {\n            id: 0,\n            wxid: \"new\", //微信号\n            initial: '新的朋友', //姓名首字母\n            img: 'static/images/newfriend.jpg', //头像\n            signature: \"\", //个性签名\n            nickname: \"新的朋友\",  //昵称\n            sex: 0,   //性别 1为男，0为女\n            remark: \"新的朋友\",  //备注\n            area: \"\",  //地区\n        }\n        \n    ],\n    friendIds: [],\n    friendDatas: [],\n    //emoji表情\n    emojis: [\n        { file: '100.gif', code: '/::)', title: '微笑',reg:/\\/::\\)/g },\n        { file: '101.gif', code: '/::~', title: '伤心',reg:/\\/::~/g },\n        { file: '102.gif', code: '/::B', title: '美女',reg:/\\/::B/g },\n        { file: '103.gif', code: '/::|', title: '发呆',reg:/\\/::\\|/g },\n        { file: '104.gif', code: '/:8-)', title: '墨镜',reg:/\\/:8-\\)/g },\n        { file: '105.gif', code: '/::<', title: '哭',reg:/\\/::</g },\n        { file: '106.gif', code: '/::$', title: '羞',reg:/\\/::\\$/g },\n        { file: '107.gif', code: '/::X', title: '哑',reg:/\\/::X/g },\n        { file: '108.gif', code: '/::Z', title: '睡',reg:/\\/::Z/g },\n        { file: '109.gif', code: '/::\\'(', title: '哭',reg:/\\/::'\\(/g },\n        { file: '110.gif', code: '/::-|', title: '囧',reg:/\\/::-\\|/g },\n        { file: '111.gif', code: '/::@', title: '怒',reg:/\\/::@/g },\n        { file: '112.gif', code: '/::P', title: '调皮',reg:/\\/::P/g },\n        { file: '113.gif', code: '/::D', title: '笑',reg:/\\/::D/g },\n        { file: '114.gif', code: '/::O', title: '惊讶',reg:/\\/::O/g },\n        { file: '115.gif', code: '/::(', title: '难过',reg:/\\/::\\(/g },\n        { file: '116.gif', code: '/::+', title: '酷',reg:/\\/::\\+/g },\n        { file: '117.gif', code: '/:--b', title: '汗',reg:/\\/:--b/g },\n        { file: '118.gif', code: '/::Q', title: '抓狂',reg:/\\/::Q/g },\n        { file: '119.gif', code: '/::T', title: '吐',reg:/\\/::T/g },\n        { file: '120.gif', code: '/:,@P', title: '笑',reg:/\\/:,@P/g },\n        { file: '121.gif', code: '/:,@-D', title: '快乐',reg:/\\/:,@-D/g },\n        { file: '122.gif', code: '/::d', title: '奇',reg:/\\/::d/g },\n        { file: '123.gif', code: '/:,@o', title: '傲' ,reg:/\\/:,@o/g},\n        { file: '124.gif', code: '/::g', title: '饿',reg:/\\/::g/g },\n        { file: '125.gif', code: '/:|-)', title: '累' ,reg:/\\/:\\|-\\)/g},\n        { file: '126.gif', code: '/::!', title: '吓',reg:/\\/::!/g },\n        { file: '127.gif', code: '/::L', title: '汗',reg:/\\/::L/g },\n        { file: '128.gif', code: '/::>', title: '高兴',reg:/\\/::>/g },\n        { file: '129.gif', code: '/::,@', title: '闲',reg:/\\/::,@/g },\n        { file: '130.gif', code: '/:,@f', title: '努力',reg:/\\/:,@f/g },\n        { file: '131.gif', code: '/::-S', title: '骂',reg:/\\/::-S/g },\n        { file: '133.gif', code: '/:,@x', title: '秘密',reg:/\\/:,@x/g },\n        { file: '134.gif', code: '/:,@@', title: '乱',reg:/\\/:,@@/g },\n        { file: '135.gif', code: '/::8', title: '疯',reg:/\\/::8/g },\n        { file: '136.gif', code: '/:,@!', title: '哀',reg:/\\/:,@!/g },\n        { file: '137.gif', code: '/:!!!', title: '鬼',reg:/\\/:!!!/g },\n        { file: '138.gif', code: '/:xx', title: '打击',reg:/\\/:xx/g },\n        { file: '139.gif', code: '/:bye', title: 'bye',reg:/\\/:bye/g },\n        { file: '142.gif', code: '/:handclap', title: '鼓掌',reg:/\\/:handclap/g },\n        { file: '145.gif', code: '/:<@', title: '什么',reg:/\\/:<@/g },\n        { file: '147.gif', code: '/::-O', title: '累',reg:/\\/::-O/g },\n        { file: '153.gif', code: '/:@x', title: '吓',reg:/\\/:@x/g },\n        { file: '155.gif', code: '/:pd', title: '刀',reg:/\\/:pd/g },\n        { file: '156.gif', code: '/:<W>', title: '水果',reg:/\\/:<W>/g },\n        { file: '157.gif', code: '/:beer', title: '酒',reg:/\\/:beer/g },\n        { file: '158.gif', code: '/:basketb', title: '篮球',reg:/\\/:basketb/g },\n        { file: '159.gif', code: '/:oo', title: '乒乓',reg:/\\/:oo/g },\n        { file: '195.gif', code: '/:circle', title: '跳舞',reg:/\\/:circle/g },\n        { file: '160.gif', code: '/:coffee', title: '咖啡',reg:/\\/:coffee/g }\n     ],\n    // 得知当前选择的是哪个对话\n    selectId: 1,\n    //选择的会话target\n    selectTarget: '',\n    // 得知当前选择的是哪个好友\n    selectFriendId: 0,\n    vueSocket: null,\n    voipClient: null,\n    groupCallClient: null,\n    //会话列表\n    conversations: [],\n    //消息列表\n    messages: [],\n    //搜索用户列表\n    searchUsers: [],\n    friendRequests: [],\n    newFriendRequestCount: 0,\n    deviceId: '',\n    userId: '',\n    token: '',\n    userInfoList: [],\n    groupInfoList: [],\n    tempGroupMembers: [],\n    notify:'',\n    inCommingNotify:'',\n    outGoingNotify:'',\n    firstLogin: false,\n    emptyMessage: false,\n    //修改全屏模式\n    changeFullScreenMode: false,\n    appHeight: 638,\n    visibilityState: 'hidden',\n    //是否限制音视频对话框\n    showChatBox: false,\n    showAudioBox: false,\n    showSearchFriendDialog: false,\n    showCreateGroupDialog: false,\n    showRelayMessageDialog: false,\n    showGroupCallVideoDialog: false,\n    groupCallMembers: [],\n    showGroupInfo: false,\n    showMessageRightMenu: [],\n    currentRightMenuMessage: null,\n    //待请求用户id信息列表\n    waitUserIds: [],\n    //0创建群组，1,添加群组人员，2,移除群组人员 3 单聊用户创建群组 4 创建群组音视频聊天\n    groupOperateState: 0,\n    groupMemberMap: new Map(),\n    groupMemberTracker: 0,\n    isLoadRemoteMessage: false\n}\n\nconst mutations = {\n    // 从localStorage 中获取数据\n    initData (state) {\n        state.userId = localStorage.getItem('vue-user-id');\n        state.token = localStorage.getItem('vue-token');\n        const vueSocket = new VueWebSocket();\n        state.vueSocket = vueSocket;\n        //voip client\n        state.voipClient = new VoipClient(store);\n        state.groupCallClient = new GroupCallClient(store)\n        let conversations = LocalStore.getConversations();\n        if(conversations){\n            state.conversations = conversations;\n        }\n        let messages = LocalStore.getMessages();\n        if(messages){\n            state.messages = messages;\n        }\n        state.selectTarget = LocalStore.getSelectTarget();\n        let userInfoList = LocalStore.getUserInfoList();\n        if(userInfoList){\n            state.userInfoList = userInfoList;\n        }\n        state.notify = new Notify({\n            effect: 'flash',\n            interval: 500,\n            onclick: () => {\n                console.log('on click');\n                state.notify.close();\n            },\n            audio:{\n                file: ['/static/audio/notify.mp3']\n            }\n        });\n        \n        state.inCommingNotify =  new Notify({\n            audio:{\n                file: ['/static/audio/incoming_call_ring.mp3']\n            }\n        });\n\n        state.outGoingNotify = new Notify({\n            audio:{\n                file: ['/static/audio/outgoing_call_ring.mp3']\n            }\n        });\n\n    },\n    // 获取搜索值\n\tsearch (state, value) {\n       state.searchText = value\n    },\n    // 得知用户当前选择的是哪个对话。便于匹配对应的对话框\n    selectSession (state, value) {\n       state.selectId = value\n    },\n    selectConversation(state,value){\n       state.selectTarget = value;\n       //清除未读数\n       var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === value);\n       if(stateConversationInfo && stateConversationInfo.conversationInfo.unreadCount){\n         stateConversationInfo.conversationInfo.unreadCount.unread = 0;\n       }\n    },\n\n    clearUnreadStatus(state){\n        var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === state.selectTarget);\n        if(stateConversationInfo && stateConversationInfo.conversationInfo.unreadCount){\n          stateConversationInfo.conversationInfo.unreadCount.unread = 0;\n        }\n    },\n    // 得知用户当前选择的是哪个好友。\n    selectFriend (state, value) {\n       state.selectFriendId = value\n       console.log(\"select friend id \"+value);\n       if(value === 0){\n           if(state.friendRequests.length == 0){\n             state.vueSocket.getFriendRequest(LocalStore.getFriendRequestVersion());\n           } else {\n             state.newFriendRequestCount = 0;\n           }\n       }\n    },\n\n    //更新朋友列表\n    updateFriendList(state,value){\n        if(state.friendlist.length === 0){\n           state.friendlist.push({\n            id: 0,\n            wxid: \"new\", //微信号\n            initial: '新的朋友', //姓名首字母\n            img: 'static/images/newfriend.jpg', //头像\n            signature: \"\", //个性签名\n            nickname: \"新的朋友\",  //昵称\n            sex: 0,   //性别 1为男，0为女\n            remark: \"新的朋友\",  //备注\n            area: \"\",  //地区\n            });\n        }\n        for(var i in value){\n            var currentUser = value[i];\n            if(currentUser.wxid != state.userId){\n                var friendUid = state.friendIds.find(friendUid => friendUid === currentUser.wxid);\n                if(friendUid){\n                  var friendData = state.friendDatas.find(friend => friend.friendUid == friendUid)\n                  if(friendData && friendData.alias && friendData.alias != \"\"){\n                     currentUser.remark = friendData.alias\n                  }  \n                  var isExist = false;\n                  for(var friend of state.friendlist){\n                    if(friend.wxid === currentUser.wxid){\n                        isExist = true;\n                        friend.nickname = currentUser.nickname;\n                        friend.img = currentUser.img;\n                        friend.remark = currentUser.remark;\n                    }\n                  }\n                  if(!isExist){\n                    currentUser.id = state.friendlist.length\n                    state.friendlist.push(currentUser);\n                  }\n                }\n            }  \n        }\n        //更新会话信息\n        for(var stateConversationInfo of state.conversations){\n            var friend = state.friendlist.find(friend => friend.wxid === stateConversationInfo.conversationInfo.target);\n            if(friend){\n                stateConversationInfo.name = friend.remark ? friend.remark: friend.nickname;\n                stateConversationInfo.img = friend.img;\n            }\n        }\n\n        //更新消息列表信息\n        for(var stateChatMessage of state.messages){\n            var friend = state.friendlist.find(friend => friend.wxid === stateChatMessage.target);\n            if(friend){\n                stateChatMessage.name = friend.remark ? friend.remark: friend.nickname;\n            }\n        }\n    },\n\n    updateConversationBrief(state){\n        //更新会话信息\n        for(var stateConversationInfo of state.conversations){\n            var friend = state.friendlist.find(friend => friend.wxid === stateConversationInfo.conversationInfo.target);\n            if(friend){\n                stateConversationInfo.name = friend.remark ? friend.remark: friend.nickname;\n                stateConversationInfo.img = friend.img;\n            } else {\n                var user = state.userInfoList.find(user => user.uid == stateConversationInfo.conversationInfo.target)\n                if(user){\n                    stateConversationInfo.name = user.displayName;\n                    stateConversationInfo.img = user.portrait;\n                }\n            }\n        }\n    },\n\n    updateMessageBrief(state){\n        //更新消息列表信息\n        for(var stateChatMessage of state.messages){\n            var friend = state.friendlist.find(friend => friend.wxid === stateChatMessage.target);\n            if(friend){\n                stateChatMessage.name = friend.remark ? friend.remark: friend.nickname;\n            }else {\n                var user = state.userInfoList.find(user => user.uid == stateChatMessage.target)\n                if(user){\n                    stateChatMessage.name = user.displayName;\n                }\n            }\n        }\n    },\n\n    updateUserInfos(state,userInfos){\n        for(let currentUserInfo of userInfos){\n           if(currentUserInfo.uid === state.userId){\n               state.user.img = currentUserInfo.portrait;\n               state.user.name = currentUserInfo.displayName;\n           }\n           var isExist = false;\n           var deleteIndex = 0;\n           for(var index in state.userInfoList){\n               var userInfo = state.userInfoList[index];\n               if(userInfo.uid == currentUserInfo.uid){\n                   isExist = true;\n                   deleteIndex = index;\n                   break;\n               }\n           }\n           if(isExist){\n              state.userInfoList.splice(deleteIndex,1,currentUserInfo)\n           } else {\n              state.userInfoList.push(currentUserInfo);\n           }\n        }\n        this.commit(\"updateConversationBrief\")\n        this.commit(\"updateMessageBrief\")\n    },\n\n    updateGroupInfos(state,groupInfos){\n        for(let currentGroupInfo of groupInfos){\n           var isExist = false;\n           var deleteIndex = 0;\n           for(var index in state.groupInfoList){\n               var groupInfo = state.groupInfoList[index];\n               if(groupInfo.target == currentGroupInfo.target){\n                    isExist = true;\n                    deleteIndex = index;\n                    break;\n               }\n           }\n           if(isExist){\n              state.groupInfoList.splice(deleteIndex,1,currentGroupInfo);\n           } else {\n               state.groupInfoList.push(currentGroupInfo);\n           }\n        }\n        console.log(\"group size \"+state.groupInfoList.length);\n        this.commit(\"updateConversationIntro\",groupInfos);\n    },\n\n    getGroupInfo(state,target){\n        state.vueSocket.getGroupInfo(target,false);\n    },\n\n    getGroupMember(state,groupId){\n        state.vueSocket.getGroupMember(groupId,true);\n    },\n\n    quitGroup(state,groupId){\n        state.vueSocket.quitGroup(groupId);\n    },\n\n    deleteConversation(state,groupId){\n        //为防止再次收到消息，退出群组的发起人不应该在接收任何退出群组消息，防止再次产生会话\n        var index = -1\n        for(var i = 0; i<state.conversations.length; i++){\n            if(state.conversations[i].conversationInfo.target == groupId){\n                index = i;\n                break;\n            }\n        }\n        console.log(\"quit group index \"+index)\n        if(index != -1){\n            state.conversations.splice(index,1);\n        }\n        //重新选择新的会话\n        if(state.conversations.length > 0){\n            state.selectTarget = state.conversations[0].conversationInfo.target\n        }\n    },\n\n    updateTempGroupMember(state,groupMembers){\n        state.tempGroupMembers = groupMembers;\n    },\n\n    // 发送信息\n    sendMessage (state, sendMessage){\n        state.isLoadRemoteMessage = false;\n        var message = Message.toMessage(state,sendMessage);\n        var protoMessage = ProtoMessage.convertToProtoMessage(message);\n        console.log(\"send protomessage \"+JSON.stringify(protoMessage));\n        \n        // if(MessageConfig.isDisplayableMessage(protoMessage)){\n        //     var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === protoMessage.target);\n        //     stateConversationInfo.conversationInfo.lastMessage = protoMessage;\n        //     stateConversationInfo.conversationInfo.timestamp = protoMessage.timestamp;\n\n        //     var stateChatMessage = state.messages.find(chatmessage => chatmessage.target === protoMessage.target);\n        //     if(!stateChatMessage){\n        //         stateChatMessage = new StateSelectChateMessage();\n        //         stateChatMessage.target = protoMessage.target;\n        //         var friend = state.friendlist.find(friend => friend.wxid === protoMessage.target);\n        //         if(friend != null){\n        //          stateChatMessage.name =  friend.nickname;\n        //         }\n        //         stateChatMessage.protoMessages.push(protoMessage);\n        //         state.messages.push(stateChatMessage);\n        //     } else {\n        //         stateChatMessage.protoMessages.push(protoMessage);\n        //     }\n            \n        // }\n        this.commit(\"preAddProtoMessage\",protoMessage)\n\n        //发送消息到对端\n        state.vueSocket.sendMessage(protoMessage);\n    },\n\n    //图片，视频类消息，需要先加入消息，然后上传成功后在更新message content\n    preAddProtoMessage(state,protoMessage){\n        \n        if(MessageConfig.isDisplayableMessage(protoMessage)){\n            var stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === protoMessage.target);\n            stateConversationInfo.conversationInfo.lastMessage = protoMessage;\n            stateConversationInfo.conversationInfo.timestamp = protoMessage.timestamp;\n\n            var stateChatMessage = state.messages.find(chatmessage => chatmessage.target === protoMessage.target);\n            if(!stateChatMessage){\n                stateChatMessage = new StateSelectChateMessage();\n                stateChatMessage.target = protoMessage.target;\n                var friend = state.friendlist.find(friend => friend.wxid === protoMessage.target);\n                if(friend != null){\n                 stateChatMessage.name =  friend.nickname;\n                }\n                stateChatMessage.protoMessages.push(protoMessage);\n                state.messages.push(stateChatMessage);\n            } else {\n                //限制单个会话最大消息存储总数\n                if(stateChatMessage.protoMessages.length > CONVERSATION_MAX_MESSAGE_SIZE){\n                    stateChatMessage.protoMessages.splice(0, stateChatMessage.protoMessages.length - CONVERSATION_MAX_MESSAGE_SIZE );\n                }\n                stateChatMessage.protoMessages.push(protoMessage);\n            }\n            \n        }\n    },\n\n    updateSendMessage(state,updateMessage){\n        var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);\n        if(stateChatMessage){\n            var protoMessage = stateChatMessage.protoMessages.find(message => message.messageId == updateMessage.messageId);\n            if(protoMessage){\n                protoMessage.status = MessageStatus.Sending;\n                var messagePayload = updateMessage.messageContent.encode();\n                protoMessage.content = ProtoMessageContent.toProtoMessageContent(messagePayload);\n            }\n        }\n        state.vueSocket.sendMessage(protoMessage);\n    },\n\n\n    // 选择好友后，点击发送信息。判断在聊天列表中是否有该好友，有的话跳到该好友对话。没有的话\n    // 添加该好友的对话 并置顶\n    send (state) {\n        let result = state.friendlist.find(friend => friend.id === state.selectFriendId)\n        let stateConversationInfo = state.conversations.find(stateConversationInfo => stateConversationInfo.conversationInfo.target === result.wxid)\n        if( !stateConversationInfo ){\n            state.selectTarget  = result.wxid;\n            var protoConversationInfo = new ProtoConversationInfo();\n            protoConversationInfo.conversationType = ConversationType.Single;\n            protoConversationInfo.target = result.wxid;\n            protoConversationInfo.line = 0;\n            protoConversationInfo.top = false;\n            protoConversationInfo.slient = false;\n            protoConversationInfo.timestamp = new Date().getTime();\n            protoConversationInfo.unreadCount = new UnreadCount();\n            protoConversationInfo.lastMessage = null;\n\n            var newStateConversationInfo = new StateConversationInfo();\n            newStateConversationInfo.name = result.remark;\n            newStateConversationInfo.img = result.img;\n            newStateConversationInfo.conversationInfo = protoConversationInfo;\n            state.conversations.unshift(newStateConversationInfo);\n        } else {\n            state.selectTarget = stateConversationInfo.conversationInfo.target\n            \n        }\n        router.push({ path: '/conversation'})\n    },\n\n    //更新会话列表\n    updateConversationInfo(state,protoConversationInfo){\n        var update = false;\n        var updateStateConverstaionInfo;\n        var currentConversationInfoIndex;\n        for(var index in state.conversations){\n            var stateConverstaionInfo = state.conversations[index];\n            if(stateConverstaionInfo.conversationInfo.conversationType == protoConversationInfo.conversationType \n                && stateConverstaionInfo.conversationInfo.target == protoConversationInfo.target){\n                update = true;\n                currentConversationInfoIndex = index;\n                stateConverstaionInfo.conversationInfo.lastMessage = protoConversationInfo.lastMessage;\n                stateConverstaionInfo.conversationInfo.timestamp = protoConversationInfo.lastMessage.timestamp;\n                updateStateConverstaionInfo = stateConverstaionInfo;\n                break;\n            }\n        }\n        //新消息会话置顶\n        if(update){\n            state.conversations.splice(currentConversationInfoIndex,1);\n            state.conversations.unshift(updateStateConverstaionInfo);\n        }\n        if(!update){\n           updateStateConverstaionInfo = new StateConversationInfo();\n           updateStateConverstaionInfo.conversationInfo = protoConversationInfo;\n\n            //单聊会话\n           if(protoConversationInfo.conversationType == ConversationType.Single){\n                var friend = state.friendlist.find(friend => friend.wxid === protoConversationInfo.target);\n                if(friend != null){\n                    var name = friend.nickname;\n                    var img = friend.img == null ? 'static/images/vue.jpg': friend.img;\n                    updateStateConverstaionInfo.name = name;\n                    updateStateConverstaionInfo.img = img;\n                } else {\n                    updateStateConverstaionInfo.name = protoConversationInfo.target;\n                    updateStateConverstaionInfo.img = 'static/images/vue.jpg';\n                }\n            } else {\n                //群聊会话\n                updateStateConverstaionInfo.name = protoConversationInfo.target;\n                if(!updateStateConverstaionInfo.img){\n                    state.vueSocket.getGroupInfo(protoConversationInfo.target,false);\n                }\n                updateStateConverstaionInfo.img = 'static/images/vue.jpg';\n            }\n            state.conversations.push(updateStateConverstaionInfo);\n        }\n\n        // 消息是否属于当前会话\n        var isCurrentConversationMessage = (state.selectTarget === protoConversationInfo.target);\n        var visibilityStateVisible = (state.visibilityState === 'visible');\n        console.log(\"current message \"+isCurrentConversationMessage +\" visible \"+visibilityStateVisible+\" first login \"+state.firstLogin);\n        //只显示接收消息，同一用户不同session，不再通知\n        var isShowSendingMessage = protoConversationInfo.lastMessage.direction === 1;\n        //更新会话消息未读数\n        if(!state.firstLogin && (!isCurrentConversationMessage || (isCurrentConversationMessage && !visibilityStateVisible)) && isShowSendingMessage){\n           //统计消息未读数,注意服务端暂时还没有将透传消息发送过来，原则上这里过来的消息都不是透传消息\n           var num = updateStateConverstaionInfo.conversationInfo.unreadCount.unread += 1;\n           var notifyBody = protoConversationInfo.lastMessage.content.searchableContent;\n           console.log(\"target \"+protoConversationInfo.target+\" unread count \"+num+ \" notify body \"+notifyBody);\n           if(!notifyBody){\n              notifyBody = ProtoMessageContent.typeToContent(protoConversationInfo.lastMessage.content);\n           }\n           //notify 弹框\n           if(!state.firstLogin){\n                state.notify.player();\n                state.notify.notify({\n                    title: updateStateConverstaionInfo.name, // Set notification title\n                    body: notifyBody, // Set message content\n                    icon: updateStateConverstaionInfo.img\n                });\n            }\n        }\n\n\n       \n    },\n\n    /**\n     * 更新会话简介，主要更新会话的名称与图像\n     */\n    updateConversationIntro(state,groupInfos){\n        for(var groupInfo of groupInfos){\n           var stateConverstaionInfo = state.conversations.find(stateConverstaionInfo => stateConverstaionInfo.conversationInfo.target === groupInfo.target);\n           if(stateConverstaionInfo){\n            console.log(\"update conversation name \"+stateConverstaionInfo.name);\n               stateConverstaionInfo.name = groupInfo.name;\n               if(groupInfo.portrait){\n                   stateConverstaionInfo.img = groupInfo.portrait;\n               }\n           }\n           //更新会话标题\n\n           var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === groupInfo.target);\n           if(stateChatMessage){\n               stateChatMessage.name = groupInfo.name+\"(\"+groupInfo.memberCount+\")\";\n           }\n        }\n    },\n\n    //获取用户当前会话的历史消息\n    addOldMessage(state,protoMessage){\n        state.isLoadRemoteMessage = true\n        console.log(\"add old message \"+protoMessage)\n        for(var stateChatMessage of state.messages){\n            if(protoMessage.target == stateChatMessage.target){\n                var isSameProtoMessage = stateChatMessage.protoMessages.find(message => message.messageId === protoMessage.messageId);\n                if(!isSameProtoMessage){\n                 stateChatMessage.protoMessages.unshift(protoMessage);\n                } \n            }\n        }\n    },\n\n    addProtoMessage(state,protoMessage){\n       state.isLoadRemoteMessage = false \n        //更新用户信息\n       if(state.waitUserIds.indexOf(protoMessage.from) == -1){\n           console.log(\"waiting for get userId \"+protoMessage.from);\n           state.waitUserIds.push(protoMessage.from);\n           state.vueSocket.getUserInfos([protoMessage.from]);\n       }\n\n       var added = false;\n       var isExistMessage = false;\n       for(var stateChatMessage of state.messages){\n           if(protoMessage.target == stateChatMessage.target){\n               added = true;\n               var isSameProtoMessage = stateChatMessage.protoMessages.find(message => message.messageId === protoMessage.messageId);\n               if(!isSameProtoMessage){\n                stateChatMessage.protoMessages.push(protoMessage);\n               } else {\n                   isExistMessage = true;\n               }\n           }\n       }\n       if(!added){\n          var stateChatMessage = new StateChatMessage();\n          var friend = state.friendlist.find(friend => friend.wxid === protoMessage.target);\n          if(friend != null){\n             stateChatMessage.name =  friend.nickname;\n          }\n          stateChatMessage.target = protoMessage.target;\n          stateChatMessage.protoMessages.push(protoMessage);\n          state.messages.push(stateChatMessage);\n       }\n       //console.log(\"current message \"+protoMessage.messageId +\" isExist \"+isExistMessage);\n       if(!isExistMessage){\n           var protoConversationInfo = new ProtoConversationInfo();\n           protoConversationInfo.conversationType = protoMessage.conversationType;\n           protoConversationInfo.target = protoMessage.target;\n           protoConversationInfo.line = 0;\n           protoConversationInfo.top = false;\n           protoConversationInfo.slient = false;\n           protoConversationInfo.timestamp = protoMessage.timestamp;\n           protoConversationInfo.lastMessage = protoMessage;\n           protoConversationInfo.unreadCount = new UnreadCount();\n\n           this.commit('updateConversationInfo',protoConversationInfo);\n       }\n       \n    },\n\n\n    updateProtoMessageUid(state,updateMessage){\n        var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);\n        if(stateChatMessage){\n            var protoMessage = stateChatMessage.protoMessages.find(message => message.messageId == updateMessage.messageId);\n            if(protoMessage){\n                protoMessage.messageUid = updateMessage.messageUid;\n                return\n            }\n        } \n        //如果切换聊天，需要全局遍历，暂定\n        for(var stateChatMessage of state.messages){\n            for(var protoMessage of stateChatMessage.protoMessages){\n                if(protoMessage.messageId == updateMessage.messageId){\n                     protoMessage.messageUid = updateMessage.messageUid;\n                }\n            }\n        }\n    },\n\n\n    updateMessageStatus(state,updateMessageStatus){\n        var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);\n        if(stateChatMessage){\n            var protoMessage = stateChatMessage.protoMessages.find(message => message.messageId == updateMessageStatus.messageId);\n            if(protoMessage){\n                protoMessage.status = updateMessageStatus.status;\n                return\n            }\n        }\n        //如果切换聊天，需要全局遍历，暂定。转发消息可能出现这个问题\n        for(var stateChatMessage of state.messages){\n            for(var protoMessage of stateChatMessage.protoMessages){\n                if(protoMessage.messageId == updateMessageStatus.messageId){\n                    protoMessage.status = updateMessageStatus.status;\n                }\n            }\n        }\n    },\n\n    deleteMessage(state,messageId){\n        var stateChatMessage = state.messages.find(stateChatMessage => stateChatMessage.target === state.selectTarget);\n        if(stateChatMessage){\n            var index = -1;\n            for(var i = 0; i < stateChatMessage.protoMessages.length; i++){\n                var protoMessage = stateChatMessage.protoMessages[i]\n                if(protoMessage.messageId == messageId){\n                     index = i;\n                }\n            }\n            console.log(\"delete index \"+index+\" messageId \"+messageId)\n            if(index != -1){\n                stateChatMessage.protoMessages.splice(index,1)\n            }\n        }\n    },\n\n    updateMessageContent(state,notifyMessage){\n        var found = false;\n        for(var stateMessages of state.messages){\n             for(var protoMessage of stateMessages.protoMessages){\n                 if(protoMessage.messageUid == notifyMessage.messageUid){\n                    var recallMessageContent = new RecallMessageNotification(notifyMessage.fromUser,notifyMessage.messageUid);\n                    protoMessage.content = recallMessageContent.encode();\n                    found = true\n                    break;\n                 }\n             }\n             if(found){\n                break;\n             }\n        }\n    },\n\n    loginOut(state,message){\n        state.userId = '';\n        state.token = '';\n        localStorage.setItem(KEY_VUE_USER_ID,'');\n        localStorage.setItem(KEY_VUE_TOKEN,'');\n        state.selectTarget = '',\n        state.vueSocket.sendDisConnectMessage();\n        state.vueSocket = null;\n        state.voipClient = null;\n        state.conversations = [];\n        state.messages = [];\n        state.friendlist = [];\n        state.friendIds = [];\n        state.friendDatas = [];\n        state.selectFriendId = 0;\n        state.friendRequests = [];\n        state.waitUserIds = [];\n        state.userInfoList = [];\n        state.newFriendRequestCount = 0;\n        state.groupMemberTracker = 0;\n        state.showMessageRightMenu = [];\n        state.emptyMessage = false;\n        LocalStore.clearLocalStore();\n        ChatManager.removeOnReceiveMessageListener();\n        //发送断开消息，清除session，防止同一个设备切换登录导致的验证失败\n        router.push({path: '/login'})\n    },\n\n    changetFirstLogin(state,value){\n        console.log(\"first login \"+value);\n        state.firstLogin = value;\n    },\n\n    getUploadToken(state,value){\n       state.vueSocket.getUploadToken(value);\n    },\n\n    visibilityChange(state,value){\n       state.visibilityState = value;\n    },\n\n    searchUser(state,value){\n       state.vueSocket.searchUser(value);\n    },\n\n    updateSearchUser(state,value){\n        state.searchUsers = [];\n        for(var searchUser of value){\n            var friend = state.friendlist.find(friend => friend.wxid === searchUser.uid);\n            if(!friend && searchUser.uid !== state.userId){\n               state.searchUsers.push(searchUser);\n            }\n       }\n    },\n\n    sendFriendAddRequest(state,value){\n       state.vueSocket.sendFriendAddRequest(value);\n    },\n\n    updateFriendRequest(state,value){\n        for(var newFriendRequst of value){\n            if(newFriendRequst.status == 0 && new Date().getTime() - newFriendRequst.timestamp > 7* 24 * 60 * 60 * 1000){\n                console.log(\"friend request over time\")\n                continue\n            }\n            var friendRequest = state.friendRequests.find(friendRequest => friendRequest.from === newFriendRequst.from);\n            if(friendRequest){\n               friendRequest.status = newFriendRequst.status;\n            } else {\n                if(newFriendRequst.status == 0){\n                    state.newFriendRequestCount += 1;\n                }\n                state.friendRequests.push(newFriendRequst);\n            }\n        }\n    },\n\n    handleFriendRequest(state,value){\n        var friendRequest = state.friendRequests.find(friendRequest => friendRequest.from === value.targetUid);\n        friendRequest.status = 1;\n        state.vueSocket.handleFriendRequest(value);\n    },\n    updateFriendIds(state,friendList){\n       if(friendList){\n        state.friendDatas = friendList;   \n        var userIds = [];\n        for(var i in friendList){\n            userIds[i] = friendList[i].friendUid;\n        }\n        state.friendIds = userIds;\n       }\n    },\n    modifyMyInfo(state,value){\n        state.vueSocket.modifyMyInfo(value);\n    },\n    getUserInfos(state,value){\n        state.vueSocket.getUserInfos(value);\n    },\n    changeEmptyMessageState(state,value){\n        state.emptyMessage = value;\n    }\n\n}\nconst getters = {\n    currentGroupMembers(){\n        if(state.groupMemberMap.has(state.selectTarget)){\n            return state.groupMemberMap.get(state.selectTarget);\n        } else {\n            return []\n        }\n    },\n    //筛选会话列表\n    searchedConversationList(){\n       return state.conversations.filter(conversationInfo => conversationInfo.name ? conversationInfo.name.includes(state.searchText): false);\n    },\n    //当前会话是否为单聊会话\n    isSingleConversation(){\n       let stateConversation = state.conversations.find(stateConversation => stateConversation.conversationInfo.target === state.selectTarget);\n       if(!stateConversation){\n          return false;\n       }\n       return stateConversation.conversationInfo.conversationType === ConversationType.Single; \n    },\n    // 筛选出含有搜索值的好友列表\n    searchedFriendlist () {\n       //需要根据用户昵称拼音首字母进行分类\n       var friendMap = new Map();\n       var noInitFriendList = [];\n       var allFriendList = [];\n       for(var friendOrigin of state.friendlist){\n           var friend = {\n                id: friendOrigin.id,\n                wxid: friendOrigin.wxid, //微信号\n                initial: friendOrigin.initial, //姓名首字母\n                img: friendOrigin.img, //头像\n                signature: friendOrigin.signature, //个性签名\n                nickname: friendOrigin.nickname,  //昵称\n                sex: friendOrigin.sex,   //性别 1为男，0为女\n                remark: friendOrigin.remark,  //备注\n                area: friendOrigin.area,  //地区\n           };\n           if(friend.id == 0){\n                continue;\n           }\n           if(friend.initial){\n                var initalFriendList = friendMap.get(friend.initial);\n                if(initalFriendList){\n                   friend.initial = \"\";\n                   initalFriendList.push(friend);\n                } else {\n                    initalFriendList = [];\n                    initalFriendList.push(friend);\n                    friendMap.set(friend.initial,initalFriendList);\n                }\n           } else {\n             noInitFriendList.push(friend);\n           }\n       }\n       if(state.friendlist.length > 0){\n         allFriendList.push(state.friendlist[0]);\n       }\n       if(noInitFriendList.length > 0){\n           for(var friend of noInitFriendList){\n              allFriendList.push(friend);\n           }\n       }\n       for(var [key,friendList] of friendMap){\n           for(var friend of friendList){\n             allFriendList.push(friend);\n           }\n       }\n       let friends = allFriendList.filter(friend => friend.remark.includes(state.searchText));\n       return friends;\n    },\n    onlyFriendlist(){\n        let friends = state.friendlist.slice(1,state.friendlist.length);\n        var listunCheckedFriends = [];\n        for(var friend of friends){\n            console.log(\"friend only initial \"+friend.initial);\n           listunCheckedFriends.push({\n               id: friend.id,\n               wxid: friend.wxid,\n               remark: friend.remark,\n               img: friend.img,\n               initial: friend.initial,\n               checked: false\n           });\n        }\n        return listunCheckedFriends;\n    },\n    // 通过当前选择是哪个对话匹配相应的对话\n    selectedChat (state) {\n       let chatMessage = state.messages.find(chatMessage => chatMessage.target === state.selectTarget);\n       console.log(\"select target \"+state.selectTarget)\n       if(chatMessage == null){\n           var conversationName = \"\";\n           var conversationTarget = '';\n           if(state.friendlist){\n             var friend = state.friendlist.find(friend => friend.wxid == state.selectTarget) \n             if(friend){\n                 conversationName = friend.nickname;\n                 conversationTarget = friend.wxid;\n             }\n           }\n          chatMessage = {\n              name: conversationName,\n              target: conversationTarget,\n              protoMessages: []\n          }\n       }\n       console.log(\"selectedChat \"+chatMessage.name+\" target \"+chatMessage.target);\n       return chatMessage\n    },\n    // 通过当前选择是哪个好友匹配相应的好友\n    selectedFriend (state) {\n       let friend = state.friendlist.find(friend => friend.id === state.selectFriendId);\n       return friend\n    },\n    messages (state) {\n        let chatMessage = state.messages.find(chatMessage => chatMessage.target === state.selectTarget);\n        if(chatMessage == null){\n            return [];\n        }\n        return chatMessage.protoMessages;\n    },\n    unreadTotalCount(state){\n        var total = 0;\n        if(state.conversations){\n            for(var stateConversationInfo of state.conversations){\n                if(stateConversationInfo.conversationInfo.unreadCount){\n                    total += stateConversationInfo.conversationInfo.unreadCount.unread;\n                }\n            }\n        }\n        if(total === 0){\n            state.notify.faviconClear();\n            state.notify.setTitle();\n            state.notify.close();\n        } else {\n            state.notify.setFavicon(total)\n            state.notify.setTitle('你有新的消息未读');\n        }\n        return total;\n    },\n}\n\nconst actions = {\n\tsearch: ({ commit }, value) => {\n        setTimeout(() => {\n                commit('search', value)\n        }, 100)\n    },\n    selectSession: ({ commit }, value) => commit('selectSession', value),\n    selectConversation: ({ commit }, value) => commit('selectConversation', value),\n    clearUnreadStatus: ({ commit }, value) => commit('clearUnreadStatus', value),\n    selectFriend: ({ commit }, value) => commit('selectFriend', value),\n    updateFriendList: ({ commit }, value) => commit('updateFriendList', value),\n    updateUserInfos: ({ commit }, value) => commit('updateUserInfos', value),\n    sendMessage: ({ commit }, msg) => commit('sendMessage', msg),\n    send: ({ commit }) => commit('send'),\n    initData: ({ commit }) => commit('initData'),\n    updateConversationInfo: ({ commit }, value) => commit('updateConversationInfo', value),\n    updateConversationIntro: ({ commit }, value) => commit('updateConversationIntro', value),\n    addProtoMessage: ({ commit }, value) => commit('addProtoMessage', value),\n    loginOut: ({ commit }, value) => commit('loginOut', value),\n    changetFirstLogin: ({ commit }, value) => commit('changetFirstLogin', value),\n    getUploadToken: ({ commit }, value) => commit('getUploadToken', value),\n    visibilityChange: ({ commit }, value) => commit('visibilityChange', value),\n    searchUser: ({ commit }, value) => commit('searchUser', value),\n    updateSearchUser: ({ commit }, value) => commit('updateSearchUser', value),\n    sendFriendAddRequest: ({ commit }, value) => commit('sendFriendAddRequest', value),\n    updateFriendRequest: ({ commit }, value) => commit('updateFriendRequest', value),\n    handleFriendRequest: ({ commit }, value) => commit('handleFriendRequest', value),\n    updateFriendIds: ({ commit }, value) => commit('updateFriendIds', value),\n    modifyMyInfo: ({ commit }, value) => commit('modifyMyInfo', value),\n    getUserInfos: ({ commit }, value) => commit('getUserInfos', value),\n    updateGroupInfos: ({ commit }, value) => commit('updateGroupInfos', value),\n    getGroupInfo: ({ commit }, value) => commit('getGroupInfo', value),\n    getGroupMember: ({ commit }, value) => commit('getGroupMember', value),\n    quitGroup: ({ commit }, value) => commit('quitGroup', value),\n    updateTempGroupMember: ({ commit }, value) => commit('updateTempGroupMember', value),\n    changeEmptyMessageState: ({ commit }, value) => commit('changeEmptyMessageState', value),\n    updateProtoMessageUid: ({ commit }, value) => commit('updateProtoMessageUid', value),\n    updateMessageStatus: ({ commit }, value) => commit('updateMessageStatus', value),\n    updateMessageContent: ({ commit }, value) => commit('updateMessageContent', value),\n    deleteConversation: ({ commit }, value) => commit('deleteConversation', value),\n    deleteMessage: ({ commit }, value) => commit('deleteMessage', value),\n    preAddProtoMessage: ({ commit }, value) => commit('preAddProtoMessage', value),\n    updateSendMessage: ({ commit }, value) => commit('updateSendMessage', value),\n    addOldMessage: ({ commit }, value) => commit('addOldMessage', value),\n}\nconst store = new Vuex.Store({\n  state,\n  mutations,\n  getters,\n  actions\n})\n\nstore.watch(\n    state => state.conversations,\n    value => {\n        LocalStore.saveConverSations(value);\n    },\n    {\n        deep : true\n    }\n)\n\nstore.watch(\n    state => state.messages,\n    value => {\n        LocalStore.saveMessages(value);\n    },\n    {\n        deep : true\n    }\n)\n\nstore.watch(\n    state => state.userInfoList,\n    value => {\n        LocalStore.saveUserInfoList(value);\n    },\n    {\n        deep : true\n    }\n)\n\n\nstore.watch(\n    state => state.selectTarget,\n    value => {\n        LocalStore.setSelectTarget(value);\n    },\n    {\n        deep : true\n    }\n)\n\nexport default store;\n"
  },
  {
    "path": "src/webrtc/callEndReason.js",
    "content": "export default class CallEndReason {\n    static REASON_Unknown = 'unknown';\n    static REASON_Busy = 'busy';\n    static REASON_SignalError = 'signalError';\n    static REASON_Hangup = 'hangup';\n    static REASON_MediaError = 'mediaError';\n    static REASON_RemoteHangup = 'remoteHangup';\n    static REASON_OpenCameraFailure = 'openCameraError';\n    static REASON_Timeout = 'timeout';\n    static REASON_AcceptByOtherClient = 'acceptByOtherClient';\n    static REASON_AllLeft = 'allLeft';\n}"
  },
  {
    "path": "src/webrtc/callSession.js",
    "content": "import CallState from \"./callState\";\nexport default class CallSession{\n    callId;\n    clientId;\n    audioOnly;\n    startTime;\n    sessionCallback;\n    callState;\n    voipClient;\n    tos;\n\n    constructor(voipClient){\n        this.startTime = new Date().getTime();\n        this.voipClient = voipClient;\n    }\n\n    setState(state){\n       if(this.callState != state){\n            var previousState = this.callState;\n            this.callState = state;\n            console.log(\"set current call state \"+this.callState);\n            switch(state){\n                case CallState.STATUS_INCOMING:\n                case CallState.STATUS_OUTGOING:\n                    this.voipClient.currentEngineCallback.shouldStartRing(state === CallState.STATUS_INCOMING)    \n                    break;\n                case CallState.STATUS_CONNECTING:\n                    this.voipClient.currentEngineCallback.shouldSopRing()\n                    break;\n                case CallState.STATUS_IDLE:\n                case CallState.STATUS_CONNECTED:\n                    if (previousState == CallState.STATUS_INCOMING || previousState == CallState.STATUS_OUTGOING) {\n                        this.voipClient.currentEngineCallback.shouldSopRing();\n                    }\n                    break;\n\n            }\n            if(this.sessionCallback){\n                this.sessionCallback.didChangeState(this.callState);\n            }\n       }\n       \n    }\n\n    endCall(endCallReason,sender=''){\n       this.setState(CallState.STATUS_IDLE);\n       this.voipClient.closeCall();\n       this.voipClient.currentSession = null;\n       if(this.sessionCallback){\n         this.sessionCallback.didCallEndWithReason(endCallReason,sender);\n       }\n    }\n}"
  },
  {
    "path": "src/webrtc/callState.js",
    "content": "export default class CallState {\n    static STATUS_IDLE = 0;\n    static STATUS_OUTGOING = 1;\n    static STATUS_INCOMING = 2;\n    static STATUS_CONNECTING = 3;\n    static STATUS_CONNECTED = 4;\n}"
  },
  {
    "path": "src/webrtc/engineCallback.js",
    "content": "export default class EngineCallback{\n    onReceiveCall(callSession){}\n\n    shouldStartRing(isIncomming){}\n\n    shouldSopRing(){}\n}"
  },
  {
    "path": "src/webrtc/groupCallClient.js",
    "content": "import OnReceiverMessageListener from \"../websocket/listener/onReceiverMessageListener\";\nimport ChatManager from \"../websocket/chatManager\";\nimport CallStartMessageContent from \"./message/callStartMessageContent\";\nimport CallSession from \"./callSession\";\nimport CallState from \"./callState\";\nimport SendMessage from \"../websocket/message/sendMessage\";\nimport CallAnswerMessageContent from \"./message/callAnswerMessageContent\";\nimport CallSignalMessageContent from \"./message/callSignalMessageContent\";\nimport CallByeMessageContent from \"./message/callByeMessageContent\";\nimport CallEndReason from \"./callEndReason\";\nimport Participant from \"./participant\";\nimport kurentoUtils from \"kurento-utils\"\nimport MessageConfig from '../websocket/message/messageConfig'\nimport LocalStore from \"../websocket/store/localstore\";\n\nexport default class GroupCallClient extends OnReceiverMessageListener {\n    currentSession;\n    currentSessionCallback;\n    currentEngineCallback;\n    //是否群组聊天发起人\n    isInitiator;\n    participants = {};\n\n    constructor(store){\n        super();\n        this.store = store;\n        ChatManager.addReceiveMessageListener(this);\n    }\n\n    setCurrentSessionCallback(sessionCallback){\n        this.currentSessionCallback = sessionCallback;\n    }\n \n    setCurrentEngineCallback(engineCallback){\n         this.currentEngineCallback = engineCallback;\n    }\n\n    /**\n     * 开始群组音视频通话\n     * @param target 群组会话target\n     * @param tos 邀请的用户target 数组列表\n     * @param isAudioOnly 是否仅仅是音频聊天\n     */\n    startCall(target,tos,isAudioOnly){\n        this.isInitiator = true;\n        //创建session\n        var newSession = this.newSession(target,isAudioOnly,target + new Date().getTime());\n        newSession.setState(CallState.STATUS_OUTGOING);\n        this.currentSession = newSession;\n        console.log(\"create new session \"+this.currentSession.clientId+\" callId \"+this.currentSession.callId);\n        //发送callmessage\n        var callStartMessageContent = new CallStartMessageContent(newSession.callId,target,isAudioOnly);\n        this.sendMessage(target,callStartMessageContent,tos);\n    }\n\n\n    answerCall(audioOnly){\n      this.isInitiator = false;\n      console.log(\"isInitiator \"+this.isInitiator);\n      this.currentSession.setState(CallState.STATUS_CONNECTING);\n      var answerMesage = new CallAnswerMessageContent()\n      answerMesage.isAudioOnly = audioOnly;\n      answerMesage.callId = this.currentSession.callId;\n      this.sendMessage(this.currentSession.clientId,answerMesage);\n    }\n\n    /**\n     * 结束群组音视频通话\n     */\n    endCall(tos){\n      //发起者发送End call指令\n      if(this.isInitiator){\n        this.sendByeMessage(tos)\n      } else {\n         this.sendSignalMessage({\n           type: 'leaveRoom'\n         })\n      }\n\n      this.currentSession.endCall(CallEndReason.REASON_RemoteHangup,LocalStore.getUserId()); \n    }\n\n    closeCall(){\n      for(var key in this.participants){\n        this.participants[key].dispose()\n     }\n    }\n\n    sendByeMessage(tos){\n      var byeMessage = new CallByeMessageContent();\n      this.sendMessage(this.currentSession.clientId,byeMessage,tos)\n    }\n\n    sendSignalMessage(msg){\n      var callSignalMessageContent = new CallSignalMessageContent();\n              //callSignalMessageContent.callId = this.currentSession.callId;\n      callSignalMessageContent.payload = JSON.stringify(msg);\n      this.sendMessage(this.currentSession.clientId,callSignalMessageContent);\n    }\n\n    onReceiveMessage(protoMessage){\n      console.log(\" receive message \"+protoMessage.direction)\n        if(new Date().getTime() - protoMessage.timestamp < 90000 && protoMessage.direction === 1 && protoMessage.conversationType == 1){\n          let contentClazz = MessageConfig.getMessageContentClazz(protoMessage.content.type);\n          if(contentClazz){\n            let content = new contentClazz();\n            try {\n                content.decode(protoMessage.content);\n            } catch(err){\n              console.log('decode error');\n            }\n            if(this.currentSession){\n              console.log(\"current call state \"+this.currentSession.callState);\n            }\n            if(content instanceof CallStartMessageContent){\n              console.log(\"receive call startmessage\");\n              if(this.currentSession && this.currentSession.callState !== CallState.STATUS_IDLE){\n                this.rejectOtherCall(content.callId,protoMessage.from);\n              } else {\n                //这里client 要指定为group的target\n                this.currentSession = this.newSession(protoMessage.target,content.audioOnly,content.callId);\n                console.log(\"before receive call tos \"+protoMessage.tos+\" from \"+protoMessage.from)\n                //去除自己，加入对方的id\n                var tos = protoMessage.tos;\n                tos.splice(tos.findIndex(item => item === LocalStore.getUserId()), 1);\n                tos.push(protoMessage.from)\n                this.currentSession.tos = tos\n                console.log(\"after receive call tos \"+this.currentSession.tos)\n                this.currentSession.setState(CallState.STATUS_INCOMING);\n                this.currentEngineCallback.onReceiveCall(this.currentSession);\n              }\n              \n            } else if(content instanceof CallSignalMessageContent){\n              if(this.currentSession && this.currentSession.callState != CallState.STATUS_IDLE){\n                console.log(\"current state \"+this.currentSession.callState+\" call signal payload \"+content.payload);\n                this.handleSignalMsg(content.payload);\n              }\n            } else if(content instanceof CallByeMessageContent){\n                if(!this.currentSession || this.currentSession.callState === CallState.STATUS_IDLE ){\n                  return;\n                }\n                this.endCall();\n            }\n          }\n        }\n    }\n\n\n    async handleSignalMsg(payload){\n      var signalMessage = JSON.parse(payload);\n      var type = signalMessage.type;\n      console.log('message type '+type+\" name \"+signalMessage.name);\n      //新的用户来临\n      if(type === \"newParticipantArrived\"){\n          var name = signalMessage.name;\n          console.log(\"new participant name \"+name)\n          this.onNewParticipant(name)\n      } else if(type == \"existingParticipants\"){\n          var existingParticipants = signalMessage.data;\n          console.log(\"existingParticipants \"+signalMessage.data)\n          this.onExistingParticipants(signalMessage)\n      } else if(type == \"participantLeft\"){\n          var participantLeft = signalMessage.name;\n          console.log(\"participantLeft \"+participantLeft)\n          this.onParticipantLeft(participantLeft)\n      } else if(type == 'iceCandidate'){\n          this.participants[signalMessage.name].rtcPeer.addIceCandidate(signalMessage.candidate, function (error) {\n                if (error) {\n                console.error(\"Error adding candidate: \" + error);\n                return;\n                }\n          });\n      } else if(type == 'receiveVideoAnswer'){\n        this.onReceiverAnswer(signalMessage)\n\t    }\n    }\n    \n\n\n    newSession(clientId, audioOnly, callId){\n        var session = new CallSession(this);\n        session.clientId = clientId;\n        session.audioOnly = audioOnly;\n        session.callId = callId;\n        session.sessionCallback = this.currentSessionCallback;\n        return session;\n    }\n\n    sendMessage(target,messageConent,tos = ''){\n        this.store.dispatch('sendMessage', new SendMessage(target,messageConent,tos));\n    }\n\n    rejectOtherCall(callId,clientId){\n        var byeMessage = new CallByeMessageContent();\n        byeMessage.callId = callId;\n        this.log('reject other callId '+callId +\" clientId \"+clientId);\n        this.sendMessage(clientId,byeMessage);\n    }\n\n    onReceiverAnswer(result){\n      this.participants[result.name].rtcPeer.processAnswer (result.sdpAnswer, function (error) {\n        if (error) return console.error (error);\n      });\n    }\n\n\n    onExistingParticipants(msg) {\n      console.log(\"audioOnly \"+this.currentSession.audioOnly)\n      var constraints = {\n        audio : true,\n        video : {\n          mandatory : {\n            maxWidth : 320,\n            maxFrameRate : 15,\n            minFrameRate : 15\n          }\n        }\n      };\n      var currentUserId = LocalStore.getUserId();\n      var participant = new Participant(this.currentSession.clientId,currentUserId,this);\n      this.participants[currentUserId] = participant;\n      var video = participant.getVideoElement();\n      console.log(\"person video \"+video.tagName)\n    \n      var options = {\n            localVideo: video,\n            mediaConstraints: constraints,\n            onicecandidate: participant.onIceCandidate.bind(participant),\n            // iceServers: [{\"urls\":\"turn:turn.liyufan.win:3478\",\"username\":\"wfchat\",\"credential\":\"wfchat\"}]\n          }\n      var _this = this\n      participant.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options,\n        function(error) {\n          if(error) {\n            if(_this.currentSessionCallback){\n                _this.currentSessionCallback.didError(error)\n            }\n            return console.error(error)\n          }\n          if(_this.currentSessionCallback){\n             _this.currentSessionCallback.didCreateLocalVideoTrack()\n          }\n          this.generateOffer (participant.offerToReceiveVideo.bind(participant));\n      });\n      \n      for(var sender of msg.data){\n          this.receiveVideo(sender)\n      }\n    }\n\n    receiveVideo(sender) {\n      var participant = new Participant(this.currentSession.clientId,sender,this);\n      this.participants[sender] = participant;\n      var video = participant.getVideoElement();\n      console.log(\"receiveVideo \"+sender + \" video \"+video.tagName)\n\n    \n      var options = {\n          remoteVideo: video,\n          onicecandidate: participant.onIceCandidate.bind(participant),\n          // iceServers: [{\"urls\":\"turn:turn.liyufan.win:3478\",\"username\":\"wfchat\",\"credential\":\"wfchat\"}]\n        }\n      var _this = this\n      participant.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,\n          function(error) {\n            if(error) {\n              return console.error(error);\n            }\n            if(_this.currentSessionCallback){\n               _this.currentSessionCallback.didReceiveRemoteVideoTrack(null,sender)\n            }\n            this.generateOffer (participant.offerToReceiveVideo.bind(participant));\n      });;\n    }\n\n    onNewParticipant(name){\n        this.receiveVideo(name)\n    }\n\n    onParticipantLeft(name) {\n      console.log('Participant ' + name + ' left');\n      var participant = this.participants[name];\n      participant.dispose();\n      delete this.participants[name];\n      this.currentSessionCallback.didCallEndWithReason(CallEndReason.REASON_Hangup,name)\n    }\n}"
  },
  {
    "path": "src/webrtc/message/callAnswerMessageContent.js",
    "content": "import MessageContent from '../../websocket/message/messageContent';\nimport MessageContentType from '../../websocket/message/messageContentType';\nimport StringUtils from \"../../websocket/utils/StringUtil\"\n\nexport default class CallAnswerMessageContent extends MessageContent {\n    callId;\n    audioOnly;\n  \n    constructor(mentionedType = 0, mentionedTargets = []) {\n        super(MessageContentType.VOIP_CONTENT_TYPE_ACCEPT, mentionedType, mentionedTargets);\n    }\n  \n    digest() {\n        return '';\n    }\n  \n    encode() {\n        let payload = super.encode();\n        payload.content = this.callId;\n  \n        var obj;\n        if (this.audioOnly) {\n            obj = '1';\n        } else {\n            obj = '0';\n        }\n        payload.binaryContent = StringUtils.utf8_to_b64(obj);\n        return payload;\n    };\n  \n    decode(payload) {\n        super.decode(payload);\n        this.callId = payload.content;\n        let str = StringUtils.b64_to_utf8(payload.binaryContent);\n  \n        this.audioOnly = (str === '1');\n    }\n  }"
  },
  {
    "path": "src/webrtc/message/callAnswerTMessageContent.js",
    "content": "\nimport MessageContent from '../../websocket/message/messageContent';\nimport MessageContentType from '../../websocket/message/messageContentType';\nimport StringUtils from \"../../websocket/utils/StringUtil\"\n\nexport default class CallAnswerTMessageContent extends MessageContent {\n    callId;\n    audioOnly;\n  \n    constructor(mentionedType = 0, mentionedTargets = []) {\n        super(MessageContentType.VOIP_CONTENT_TYPE_ACCEPT_T, mentionedType, mentionedTargets);\n    }\n  \n    digest() {\n        return '';\n    }\n  \n    encode() {\n        let payload = super.encode();\n        payload.content = this.callId;\n  \n        var obj;\n        if (this.audioOnly) {\n            obj = '1';\n        } else {\n            obj = '0';\n        }\n        payload.binaryContent = StringUtils.utf8_to_b64(obj);\n        return payload;\n    };\n  \n    decode(payload) {\n        super.decode(payload);\n        this.callId = payload.content;\n        let str = StringUtils.b64_to_utf8(payload.binaryContent);\n  \n        this.audioOnly = (str === '1');\n    }\n  }"
  },
  {
    "path": "src/webrtc/message/callByeMessageContent.js",
    "content": "\nimport MessageContent from '../../websocket/message/messageContent';\nimport MessageContentType from '../../websocket/message/messageContentType';\nexport default class CallByeMessageContent extends MessageContent {\n    callId;\n  \n    constructor(mentionedType = 0, mentionedTargets = []) {\n        super(MessageContentType.VOIP_CONTENT_TYPE_END, mentionedType, mentionedTargets);\n    }\n  \n    digest() {\n        return '';\n    }\n  \n    encode() {\n        let payload = super.encode();\n        payload.content = this.callId;\n        return payload;\n    };\n  \n    decode(payload) {\n        super.decode(payload);\n        this.callId = payload.content;\n    }\n  }"
  },
  {
    "path": "src/webrtc/message/callModifyMessageContent.js",
    "content": "import MessageContent from '../../websocket/message/messageContent';\nimport MessageContentType from '../../websocket/message/messageContentType';\nimport StringUtils from \"../../websocket/utils/StringUtil\"\nexport default class CallModifyMessageContent extends MessageContent {\n    callId;\n    audioOnly;\n  \n    constructor(mentionedType = 0, mentionedTargets = []) {\n        super(MessageContentType.VOIP_CONTENT_TYPE_MODIFY, mentionedType, mentionedTargets);\n    }\n  \n    digest() {\n        return '';\n    }\n  \n    encode() {\n        let payload = super.encode();\n        payload.content = this.callId;\n  \n        var obj;\n        if (this.audioOnly) {\n            obj = '1';\n        } else {\n            obj = '0';\n        }\n        payload.binaryContent = StringUtils.utf8_to_b64(obj);\n        return payload;\n    };\n  \n    decode(payload) {\n        super.decode(payload);\n        this.callId = payload.content;\n        let str = StringUtils.b64_to_utf8(payload.binaryContent);\n  \n        this.audioOnly = (str === '1');\n    }\n  }"
  },
  {
    "path": "src/webrtc/message/callSignalMessageContent.js",
    "content": "import MessageContent from '../../websocket/message/messageContent';\nimport MessageContentType from '../../websocket/message/messageContentType';\nimport StringUtils from \"../../websocket/utils/StringUtil\"\nexport default class CallSignalMessageContent extends MessageContent {\n    callId;\n    payload;\n  \n    constructor(mentionedType = 0, mentionedTargets = []) {\n        super(MessageContentType.VOIP_CONTENT_TYPE_SIGNAL, mentionedType, mentionedTargets);\n    }\n  \n    digest() {\n        return '';\n    }\n  \n    encode() {\n        let payload = super.encode();\n        payload.content = this.callId;\n        payload.binaryContent = StringUtils.utf8_to_b64(this.payload);\n        return payload;\n    };\n  \n    decode(payload) {\n        super.decode(payload);\n        this.callId = payload.content;\n        this.payload = StringUtils.b64_to_utf8(payload.binaryContent);\n    }\n  }"
  },
  {
    "path": "src/webrtc/message/callStartMessageContent.js",
    "content": "import MessageContent from '../../websocket/message/messageContent';\nimport MessageContentType from '../../websocket/message/messageContentType';\nimport StringUtils from \"../../websocket/utils/StringUtil\"\nexport default class CallStartMessageContent extends MessageContent {\n  callId;\n  targetId;\n  connectTime;\n  endTime;\n  status;\n  audioOnly;\n\n  constructor(callId, targetId, audioOnly){\n    super(MessageContentType.VOIP_CONTENT_TYPE_START);\n    this.callId = callId;\n    this.targetId = targetId;\n    this.audioOnly = audioOnly;\n  }\n\n  digest() {\n      if (this.audioOnly) {\n          return '[语音通话]';\n      } else {\n          return '[视频通话]';\n      }\n  }\n\n  encode() {\n      let payload = super.encode();\n      payload.content = this.callId;\n\n      let obj = {\n          c: this.connectTime,\n          e: this.endTime,\n          s: this.status,\n          a: this.audioOnly ? 1 : 0,\n          t:this.targetId\n      };\n      payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n      return payload;\n  };\n\n  decode(payload) {\n      super.decode(payload);\n      this.callId = payload.content;\n      let json = StringUtils.b64_to_utf8(payload.binaryContent);\n      let obj = JSON.parse(json);\n\n      this.connectTime = obj.c;\n      this.endTime = obj.e;\n      this.status = obj.s;\n      this.audioOnly = (obj.a === 1);\n      this.targetId = obj.t;\n  }\n}"
  },
  {
    "path": "src/webrtc/participant.js",
    "content": "import CallSignalMessageContent from \"./message/callSignalMessageContent\";\n\nexport default class Participant {\n    target;\n    sender;\n    groupCallClient;\n    rtcPeer;\n\n    constructor(target,sender,groupCallClient){\n        this.target = target;\n        this.sender = sender;\n        this.groupCallClient = groupCallClient;\n    }\n\n    \n    getVideoElement(){\n      return document.getElementById(this.sender);\n    }\n\n    onIceCandidate(candidate, wp) {\n        console.log(\"Local candidate\" + JSON.stringify(candidate));\n\n        var message = {\n          type: 'onIceCandidate',\n          candidate: candidate,\n          name: name\n        };\n        this.groupCallClient.sendSignalMessage(message);\n    }\n\n    offerToReceiveVideo(error, offerSdp, wp){\n      if (error) return console.error (\"sdp offer error\"+error)\n      console.log(this.sender + ' Invoking SDP offer callback function');\n      var msg =  { \n          type : \"receiveVideoFrom\",\n          sender : this.sender,\n          sdpOffer : offerSdp\n      };\n      this.groupCallClient.sendSignalMessage(msg);\n    }\n    \n    dispose() {\n      console.log('Disposing participant ' + this.sender);\n      this.rtcPeer.dispose();\n    }\n    \n    // sendSignalMessage(msg){\n    //   var callSignalMessageContent = new CallSignalMessageContent();\n    //           //callSignalMessageContent.callId = this.currentSession.callId;\n    //   callSignalMessageContent.payload = JSON.stringify(msg);\n    //   this.groupCallClient.sendMessage(this.target,callSignalMessageContent);\n    // }\n}"
  },
  {
    "path": "src/webrtc/sessionCallback.js",
    "content": "export default class SessionCallback{\n    didCallEndWithReason(callEndReason,sender = ''){}\n\n    didChangeState(callState){}\n\n    didChangeMode(mode){}\n\n    didCreateLocalVideoTrack(stream){}\n\n    didReceiveRemoteVideoTrack(stream,sender = ''){}\n\n    didReceiveRemoteAudioTrack(stream){}\n\n    didError(error){}\n\n    didGetStats(stats){}\n}"
  },
  {
    "path": "src/webrtc/voipclient.js",
    "content": "import CallStartMessageContent from \"./message/callStartMessageContent\";\nimport ChatManager  from \"../websocket/chatManager\";\nimport OnReceiverMessageListener from \"../websocket/listener/onReceiverMessageListener\";\nimport MessageConfig from '../websocket/message/messageConfig'\nimport CallAnswerMessageContent from \"./message/callAnswerMessageContent\";\nimport CallSignalMessageContent from \"./message/callSignalMessageContent\";\nimport CallAnswerTMessageContent from \"./message/callAnswerTMessageContent\";\nimport CallSession from \"./callSession\";\nimport CallByeMessageContent from \"./message/callByeMessageContent\";\nimport SendMessage from \"../websocket/message/sendMessage\";\nimport CallState from \"./callState\";\nimport CallEndReason from \"./callEndReason\";\n\nexport default class VoipClient extends OnReceiverMessageListener{\n  \n    myPeerConnection;\n    webcamStream;\n    mediaConstraints = {\n        audio: true,            // We want an audio track\n        video: true\n      };\n    sender;\n    //是否为createOff发起人\n    isInitiator;\n    //当前会话\n    currentSession;\n    currentSessionCallback;\n    currentEngineCallback;\n\n    constructor(store){\n      super();\n      this.store = store;\n      ChatManager.addReceiveMessageListener(this);\n    }\n    \n    setCurrentSessionCallback(sessionCallback){\n       this.currentSessionCallback = sessionCallback;\n    }\n\n    setCurrentEngineCallback(engineCallback){\n        this.currentEngineCallback = engineCallback;\n    }\n\n    startCall(target,isAudioOnly){\n        this.isInitiator = false;\n        //创建session\n        var newSession = this.newSession(target,isAudioOnly,target + new Date().getTime());\n        newSession.setState(CallState.STATUS_OUTGOING);\n        this.currentSession = newSession;\n        console.log(\"create new session \"+this.currentSession.clientId+\" callId \"+this.currentSession.callId);\n        //发送callmessage\n        var callStartMessageContent = new CallStartMessageContent(newSession.callId,target,isAudioOnly);\n        this.offerMessage(target,callStartMessageContent);\n        //如果时视频，启动预览\n        this.startPreview();\n    }\n\n    cancelCall(){\n      var byeMessage = new CallByeMessageContent();\n      byeMessage.callId = this.currentSession.callId;\n      this.offerMessage(this.currentSession.clientId,byeMessage);\n      console.log(\"send bye message\");\n      this.currentSession.endCall(CallEndReason.REASON_RemoteHangup);\n      this.currentSession = null;\n    }\n\n    answerCall(audioOnly){\n        this.isInitiator = true;\n        console.log(\"isInitiator \"+this.isInitiator);\n        this.currentSession.setState(CallState.STATUS_CONNECTING);\n        var answerTMesage = new CallAnswerTMessageContent()\n        answerTMesage.isAudioOnly = audioOnly;\n        answerTMesage.callId = this.currentSession.callId;\n        this.offerMessage(this.currentSession.clientId,answerTMesage);\n        this.startPreview();\n    }\n\n    newSession(clientId, audioOnly, callId){\n       var session = new CallSession(this);\n       session.clientId = clientId;\n       session.audioOnly = audioOnly;\n       session.callId = callId;\n       session.sessionCallback = this.currentSessionCallback;\n       return session;\n    }\n    \n    rejectOtherCall(callId,clientId){\n      var byeMessage = new CallByeMessageContent();\n      byeMessage.callId = callId;\n      this.log('reject other callId '+callId +\" clientId \"+clientId);\n      this.offerMessage(clientId,byeMessage);\n    }\n\n    offerMessage(target,messageConent){\n        this.store.dispatch('sendMessage', new SendMessage(target,messageConent));\n    }\n\n    offerMessageByType(type){\n      var callSignalMessageContent = new CallSignalMessageContent();\n              callSignalMessageContent.callId = this.currentSession.callId;\n              var jsonPayload = {\n                 type: type,\n                 sdp: this.myPeerConnection.localDescription.sdp\n              }\n      callSignalMessageContent.payload = JSON.stringify(jsonPayload);\n      this.offerMessage(this.currentSession.clientId,callSignalMessageContent);\n    }\n\n    /**\n     * 接收信令服务传递过来的消息\n     */\n    onReceiveMessage(protoMessage){\n      //只处理接收消息，对于同一用户不同session会话忽略\n      if(new Date().getTime() - protoMessage.timestamp < 90000 && protoMessage.direction === 1 && protoMessage.conversationType == 0){\n        let contentClazz = MessageConfig.getMessageContentClazz(protoMessage.content.type);\n        if(contentClazz){\n          let content = new contentClazz();\n          try {\n              content.decode(protoMessage.content);\n          } catch(err){\n            console.log('decode error');\n          }\n          if(this.currentSession){\n            console.log(\"current call state \"+this.currentSession.callState);\n          }\n          if(content instanceof CallStartMessageContent){\n            console.log(\"receive call startmessage\");\n            if(this.currentSession && this.currentSession.callState !== CallState.STATUS_IDLE){\n              this.rejectOtherCall(content.callId,protoMessage.from);\n            } else {\n              this.currentSession = this.newSession(protoMessage.from,content.audioOnly,content.callId);\n              this.currentSession.setState(CallState.STATUS_INCOMING);\n              this.currentEngineCallback.onReceiveCall(this.currentSession);\n            }\n            \n          } else if(content instanceof CallAnswerMessageContent || content instanceof CallAnswerTMessageContent){\n            this.isInitiator = false;\n            if(this.currentSession  && this.currentSession.callState != CallState.STATUS_IDLE){\n              console.log(\" CallAnswerMessageContent callState \"+this.currentSession.callState);\n              if(protoMessage.from === this.currentSession.clientId && content.callId === this.currentSession.callId){\n                if(this.currentSession.callState != CallState.STATUS_OUTGOING){\n\n                  // this.rejectOtherCall(this.currentSession.callId,this.currentSession.clientId);\n                } else if(this.currentSession.callState === CallState.STATUS_OUTGOING){\n                  this.currentSession.setState(CallState.STATUS_CONNECTING);\n                }\n              }\n            }\n          } else if(content instanceof CallSignalMessageContent){\n            if(this.currentSession && this.currentSession.callState != CallState.STATUS_IDLE){\n              console.log(\"current state \"+this.currentSession.callState+\" call signal payload \"+content.payload);\n              if(this.currentSession.callState === CallState.STATUS_CONNECTING || this.currentSession.callState === CallState.STATUS_CONNECTED){\n                if(protoMessage.from === this.currentSession.clientId && content.callId === this.currentSession.callId){\n                  this.handleSignalMsg(content.payload);\n                }\n              } else {\n                 this.currentSession.endCall(CallEndReason.REASON_AcceptByOtherClient);\n                // this.currentSession.sessionCallback.didCallEndWithReason(CallEndReason.REASON_AcceptByOtherClient);\n              }\n            }\n          } else if(content instanceof CallByeMessageContent){\n             if(!this.currentSession || this.currentSession.callState === CallState.STATUS_IDLE || protoMessage.from != this.currentSession.clientId || content.callId != this.currentSession.callId){\n               return;\n             }\n             this.cancelCall();\n          }\n        }\n      }\n    }\n\n    async handleSignalMsg(payload){\n        var signalMessage = JSON.parse(payload);\n        var type = signalMessage.type;\n        console.log('message type '+type);\n        if(type === \"candidate\"){\n          var rTCIceCandidateInit = {\n            candidate: signalMessage.candidate,\n            sdpMLineIndex: signalMessage.label,\n            sdpMid: signalMessage.id\n          }\n          var candidate = new RTCIceCandidate(rTCIceCandidateInit);\n          this.log(\"*** Adding received ICE candidate: \" + JSON.stringify(candidate));\n          try {\n            await this.myPeerConnection.addIceCandidate(candidate);\n          } catch(err){\n             this.reportError(err);\n          }\n          \n        } else if(type === 'remove-candidates'){\n           this.log(\"remove candidates \");\n        } else {\n          var desc = new RTCSessionDescription(signalMessage);\n          if(type === 'answer'){\n              await this.myPeerConnection.setRemoteDescription(desc);\n          } else if(type === 'offer'){\n              await this.myPeerConnection.setRemoteDescription(desc);\n              await this.myPeerConnection.setLocalDescription(await this.myPeerConnection.createAnswer());\n              //send answer message\n              var callSignalMessageContent = new CallSignalMessageContent();\n              callSignalMessageContent.callId = this.currentSession.callId;\n              var jsonPayload = {\n                 type: 'answer',\n                 sdp: this.myPeerConnection.localDescription.sdp\n              }\n              callSignalMessageContent.payload = JSON.stringify(jsonPayload);\n              this.offerMessage(this.currentSession.clientId,callSignalMessageContent);\n          }\n        }\n    }\n\n    async handleOfferMessage(){\n      console.log(\"*** Negotiation needed \"+this.isInitiator);\n      if(!this.isInitiator){\n        return;\n      }\n      try {\n        console.log(\"---> Creating offer\");\n        const offer = await this.myPeerConnection.createOffer();\n    \n        // If the connection hasn't yet achieved the \"stable\" state,\n        // return to the caller. Another negotiationneeded event\n        // will be fired when the state stabilizes.\n    \n        if (this.myPeerConnection.signalingState != \"stable\") {\n          console.log(\"     -- The connection isn't stable yet; postponing...\")\n          return;\n        }\n    \n        // Establish the offer as the local peer's current\n        // description.\n    \n        console.log(\"---> Setting local description to the offer\");\n        await this.myPeerConnection.setLocalDescription(offer);\n    \n        // Send the offer to the remote peer.\n    \n        console.log(\"---> Sending the offer to the remote peer\");\n        this.offerMessageByType('offer');\n      } catch(err) {\n        console.log(\"*** The following error occurred while handling the negotiationneeded event:\");\n        this.reportError(err);\n      };\n    }\n\n    async startPreview(){\n      this.log(\"Starting to prepare an invitation\");\n      if (this.myPeerConnection) {\n        alert(\"You can't start a call because you already have one open!\");\n      } else {\n        \n    \n        // Get access to the webcam stream and attach it to the\n        // \"preview\" box (id \"local_video\").\n    \n        try {\n          this.mediaConstraints = {\n            audio: true,            // We want an audio track\n            video: !this.currentSession.isAudioOnly\n          }\n          this.webcamStream = await navigator.mediaDevices.getUserMedia(this.mediaConstraints);\n          if(!this.currentSession.isAudioOnly){\n            this.currentSessionCallback.didCreateLocalVideoTrack(this.webcamStream);\n          }\n          \n        } catch(err) {\n          this.handleGetUserMediaError(err);\n          return;\n        }\n\n        // Call createPeerConnection() to create the RTCPeerConnection.\n        // When this returns, myPeerConnection is our RTCPeerConnection\n        // and webcamStream is a stream coming from the camera. They are\n        // not linked together in any way yet.\n    \n        this.createPeerConnection();\n    \n        // Add the tracks from the stream to the RTCPeerConnection\n    \n        try {\n          this.webcamStream.getTracks().forEach(\n            track => this.sender = this.myPeerConnection.addTrack(track, this.webcamStream)\n          );\n        } catch(err) {\n          this.handleGetUserMediaError(err);\n        }\n      }\n    }\n\n    async createPeerConnection() {\n        this.log(\"Setting up a connection...\");\n      \n        // Create an RTCPeerConnection which knows to use our chosen\n        // STUN server.\n      \n        this.myPeerConnection = new RTCPeerConnection({\n          iceServers: [     // Information about ICE servers - Use your own!\n            {\n              urls: \"turn:turn.fsharechat.cn:3478\",  // A TURN server\n              username: \"comsince\",\n              credential: \"comsince\"\n            }\n          ]\n        });\n      \n        // Set up event handlers for the ICE negotiation process.\n      \n        this.myPeerConnection.onicecandidate = this.handleICECandidateEvent;\n        this.myPeerConnection.oniceconnectionstatechange = this.handleICEConnectionStateChangeEvent;\n        this.myPeerConnection.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent;\n        this.myPeerConnection.onsignalingstatechange = this.handleSignalingStateChangeEvent;\n        this.myPeerConnection.onnegotiationneeded = this.handleNegotiationNeededEvent;\n        this.myPeerConnection.ontrack = this.handleTrackEvent;\n      }\n\n\n      // Called by the WebRTC layer to let us know when it's time to\n     // begin, resume, or restart ICE negotiation.\n\n    handleNegotiationNeededEvent = () =>  {\n       this.handleOfferMessage();\n    }\n\n\n    // Handles |icecandidate| events by forwarding the specified\n   // ICE candidate (created by our local ICE agent) to the other\n   // peer through the signaling server.\n    //接收来自信令服务器发送来的ICE candidate事件消息\n    handleICECandidateEvent = (event) => {\n        if (event.candidate) {\n          console.log(\"*** Outgoing ICE candidate: \" + event.candidate.candidate);\n          var candidateMessageContent = new CallSignalMessageContent();\n          candidateMessageContent.callId = this.currentSession.callId;\n          var candidatePayload = {\n            type: 'candidate',\n            label: event.candidate.sdpMLineIndex,\n            id: event.candidate.sdpMid,\n            candidate: event.candidate.candidate\n          }\n          candidateMessageContent.payload = JSON.stringify(candidatePayload);\n          this.offerMessage(this.currentSession.clientId,candidateMessageContent);\n        }\n    }\n\n\n    // Handle |iceconnectionstatechange| events. This will detect\n    // when the ICE connection is closed, failed, or disconnected.\n    //\n    // This is called when the state of the ICE agent changes.\n\n    handleICEConnectionStateChangeEvent = (event) => {\n      console.log(\"*** ICE connection state changed to \" + this.myPeerConnection.iceConnectionState);\n    \n        switch(this.myPeerConnection.iceConnectionState) {\n            case \"connected\":\n                this.currentSession.setState(CallState.STATUS_CONNECTED); \n              break;\n            case \"closed\":\n            case \"failed\":\n            case \"disconnected\":\n                this.cancelCall();\n                break;\n        }\n    }\n\n\n    // Set up a |signalingstatechange| event handler. This will detect when\n// the signaling connection is closed.\n//\n// NOTE: This will actually move to the new RTCPeerConnectionState enum\n// returned in the property RTCPeerConnection.connectionState when\n// browsers catch up with the latest version of the specification!\n\n    handleSignalingStateChangeEvent = (event) => {\n      console.log(\"*** WebRTC signaling state changed to: \" + this.myPeerConnection.signalingState);\n       switch(this.myPeerConnection.signalingState) {\n         case \"closed\":\n            this.cancelCall();\n            break;\n        }\n    }\n\n\n    // Called by the WebRTC layer when events occur on the media tracks\n// on our WebRTC call. This includes when streams are added to and\n// removed from the call.\n//\n// track events include the following fields:\n//\n// RTCRtpReceiver       receiver\n// MediaStreamTrack     track\n// MediaStream[]        streams\n// RTCRtpTransceiver    transceiver\n//\n// In our case, we're just taking the first stream found and attaching\n// it to the <video> element for incoming media.\n\n    handleTrackEvent = (event) => {\n      console.log(\"comming stream\");\n      if(this.currentSession.isAudioOnly){\n         this.currentSessionCallback.didReceiveRemoteAudioTrack(event.streams[0]);\n      } else {\n        this.currentSessionCallback.didReceiveRemoteVideoTrack(event.streams[0]);\n      }\n    }\n\n\n    handleICEGatheringStateChangeEvent = (event) => {\n       console.log(\"*** ICE gathering state changed to: \" + event);\n    }\n\n\n    // Close the RTCPeerConnection and reset variables so that the user can\n// make or receive another call if they wish. This is called both\n// when the user hangs up, the other user hangs up, or if a connection\n// failure is detected.\n\n    closeCall() {\n        this.log(\"Closing the call\");\n        // Close the RTCPeerConnection\n    \n        if (this.myPeerConnection) {\n            this.log(\"--> Closing the peer connection\");\n        \n            // Disconnect all our event listeners; we don't want stray events\n            // to interfere with the hangup while it's ongoing.\n        \n            this.myPeerConnection.ontrack = null;\n            this.myPeerConnection.onnicecandidate = null;\n            this.myPeerConnection.oniceconnectionstatechange = null;\n            this.myPeerConnection.onsignalingstatechange = null;\n            this.myPeerConnection.onicegatheringstatechange = null;\n            this.myPeerConnection.onnotificationneeded = null;\n        \n            // Stop all transceivers on the connection\n        \n            // this.myPeerConnection.getTransceivers().forEach(transceiver => {\n            //     transceiver.stop();\n            // });\n\n            this.myPeerConnection.removeTrack(this.sender);\n        \n            // Stop the webcam preview as well by pausing the <video>\n            // element, then stopping each of the getUserMedia() tracks\n            // on it.\n\n            if(!this.currentSession.isAudioOnly){\n              var localVideo = document.getElementById(\"wxCallLocalVideo\");\n              if (localVideo.srcObject) {\n                  localVideo.pause();\n                  localVideo.srcObject.getTracks().forEach(track => {\n                  track.stop();\n                  });\n              }\n            } else {\n              this.webcamStream.getTracks.forEach(\n                track => {\n                  track.stop();\n                }\n              )\n            }\n            \n        \n            // Close the peer connection\n        \n            this.myPeerConnection.close();\n            this.myPeerConnection = null;\n            this.webcamStream = null;\n        }\n    \n        // Disable the hangup button\n    \n        // document.getElementById(\"hangup-button\").disabled = true;\n        // targetUsername = null;\n    }\n\n\n    // Handle errors which occur when trying to access the local media\n   // hardware; that is, exceptions thrown by getUserMedia(). The two most\n   // likely scenarios are that the user has no camera and/or microphone\n   // or that they declined to share their equipment when prompted. If\n   // they simply opted not to share their media, that's not really an\n   // error, so we won't present a message in that situation.\n\n    handleGetUserMediaError(e) {\n      this.log_error(e);\n      switch(e.name) {\n        case \"NotFoundError\":\n          alert(\"Unable to open your call because no camera and/or microphone\" +\n                \"were found.\");\n          break;\n        case \"SecurityError\":\n        case \"PermissionDeniedError\":\n          // Do nothing; this is the same as the user canceling the call.\n          break;\n        default:\n          alert(\"Error opening your camera and/or microphone: \" + e.message);\n          break;\n      }\n      this.cancelCall();\n    }\n\n\n    reportError(errMessage) {\n        this.log_error(`Error ${errMessage.name}: ${errMessage.message}`);\n    }\n\n    log_error(text) {\n       var time = new Date();\n       console.trace(\"[\" + time.toLocaleTimeString() + \"] \" + text);\n    }\n\n    log(text) {\n      var time = new Date();\n      console.log(\"[\" + time.toLocaleTimeString() + \"] \" + text);\n    }\n\n}"
  },
  {
    "path": "src/websocket/chatManager.js",
    "content": "export default class ChatManager {\n    static onReceiveMessageListeners = [];\n\n    static addReceiveMessageListener(listener){\n        this.onReceiveMessageListeners.push(listener);\n    }\n\n    static onReceiveMessage(protoMessage){\n         for(var messageListener of this.onReceiveMessageListeners){\n             messageListener.onReceiveMessage(protoMessage);\n         }\n    }\n\n    static removeOnReceiveMessageListener(){\n        ChatManager.onReceiveMessageListeners = [];\n    }\n}"
  },
  {
    "path": "src/websocket/future/futureResult.js",
    "content": "export default class FutureResult {\n    code;\n    result;\n\n    constructor(code, result){\n        this.code = code\n        this.result = result\n    }\n}"
  },
  {
    "path": "src/websocket/future/promiseResolve.js",
    "content": "export default class PromiseResolve {\n    resolve;\n    timeoutId;\n    protoMessageId;\n\n    constructor(resolve,timeoutId){\n        this.resolve = resolve;\n        this.timeoutId = timeoutId;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/abstractmessagehandler.js",
    "content": "import MessageHandler from \"./messageHandler\";\nimport Logger from \"../utils/logger\";\nimport FutureResult from '../future/futureResult';\nimport { SUCCESS_CODE, ERROR_CODE } from \"../../constant\";\n/**\n * 关于promise返回说明\n * 1.对于操作型消息，一般只需要返回成功与失败即可\n * 2.对于结果型消息，一般判断是否为空\n */\nexport default class AbstractMessageHandler extends MessageHandler{\n\n    constructor(vueWebsocket){\n        super();\n        this.vueWebsocket = vueWebsocket;\n    }\n\n    get vueWebsocketClient(){\n        return this.vueWebsocket;\n    }\n\n    processMessage(proto){\n        Logger.log(\"AbstractMessageHandler messageId \"+proto.messageId +\" proto content \"+proto.content);\n        var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId);\n        if(promiseReslove){\n            clearTimeout(promiseReslove.timeoutId);\n            if(proto.content == ''){\n                promiseReslove.resolve(new FutureResult(ERROR_CODE,proto.content));\n            } else {\n                promiseReslove.resolve(new FutureResult(SUCCESS_CODE,this.notifyContent(proto.content)));\n            }   \n            this.vueWebsocket.resolvePromiseMap.delete(proto.messageId);\n        }\n    }\n\n    notifyContent(content){\n       return content\n    }\n\n}"
  },
  {
    "path": "src/websocket/handler/addGroupMemberHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GAM } from \"../../constant\";\n\nexport default class AddGroupMemberHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GAM;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/connectackhandler.js",
    "content": "import { CONNECT_ACK } from \"../../constant\";\nimport AbstractMessageHandler from \"./abstractmessagehandler\";\nimport LocalStore from \"../store/localstore\";\n\nexport default class ConnectAckHandler extends AbstractMessageHandler{\n    constructor(vueWebsocket){\n         super(vueWebsocket);\n    }\n    match(protoObj){\n        return protoObj.signal == CONNECT_ACK;\n    }\n\n    processMessage(data){\n        console.log(\"ConnectAckHandler process message\");\n        var connectAcceptedMessage = JSON.parse(data.content);\n        console.log(\"connectAcceptedMessage friendHead \"+connectAcceptedMessage.friendHead+\" messageHead \"+connectAcceptedMessage.messageHead);\n        //拉取朋友列表\n        this.vueWebsocket.getFriend();\n        let lastMessageSeq = LocalStore.getLastMessageSeq();\n        if(!lastMessageSeq){\n            lastMessageSeq = '0';\n            this.vueWebsocket.sendAction('changetFirstLogin',true);\n        } else {\n            this.vueWebsocket.sendAction('changetFirstLogin',false);\n        }\n        //好友请求信息\n        this.vueWebsocket.getFriendRequest(LocalStore.getFriendRequestVersion());\n        //初始拉取消息列表\n        this.vueWebsocket.pullMessage(lastMessageSeq,0,0,LocalStore.getSendMessageCount());\n        LocalStore.setLastMessageSeq(connectAcceptedMessage.messageHead);\n    }\n}"
  },
  {
    "path": "src/websocket/handler/createGroupHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GC, ERROR_CODE, SUCCESS_CODE } from \"../../constant\";\n\nexport default class CreateGroupHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GC;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/dismissGroupHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GD } from \"../../constant\";\n\nexport default class DismissGroupHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GD;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/friendAddRequestHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { FAR,PUB_ACK } from \"../../constant\";\nimport Logger from \"../utils/logger\";\n\nexport default class FriendAddRequestHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == FAR;\n    }\n\n    processMessage(proto){\n        var result  = JSON.parse(proto.content);\n        Logger.log(\"friend add request result \"+result);\n    }\n}"
  },
  {
    "path": "src/websocket/handler/friendRequestHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, FRP } from \"../../constant\";\nimport LocalStore from \"../store/localstore\";\n\nexport default class FriendRequestHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == FRP;\n    }\n\n\n    processMessage(proto){\n        if(proto.content != null && proto.content != ''){\n            var friendRequests = JSON.parse(proto.content);\n            var validRequest = [];\n            var targeIds = [];\n            for(var friendRequest of friendRequests){\n               if(friendRequest.from != LocalStore.getUserId()){\n                  validRequest.push(friendRequest);\n                  targeIds.push(friendRequest.from);\n               }\n            }\n            this.vueWebsocket.getUserInfos(targeIds);\n            this.vueWebsocket.sendAction(\"updateFriendRequest\",validRequest);\n        }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/getGroupInfoHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { GPGI, PUB_ACK } from \"../../constant\";\nimport GroupInfo from \"../model/groupInfo\";\n\nexport default class GetGroupInfoHandler extends AbstractMessageHandler{\n\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GPGI;\n    }\n\n    processMessage(proto){\n        if(proto.content != null){\n           var groupInfoList = JSON.parse(proto.content);\n           var groups = [];\n           for(var groupInfo of groupInfoList){\n               groups.push(GroupInfo.convert2GroupInfo(groupInfo));\n           }\n           this.vueWebsocket.sendAction(\"updateGroupInfos\",groups);\n        }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/getGroupMemberHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GPGM } from \"../../constant\";\nimport GroupMember from \"../model/groupMember\";\n\nexport default class GetGroupMemberHandler extends AbstractMessageHandler {\n     match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GPGM;\n     }\n\n     notifyContent(content){\n         var groupMemberList = JSON.parse(content);\n         var groupMembers = [];\n         for(var groupMember of groupMemberList){\n            groupMembers.push(GroupMember.convert2GroupMember(groupMember));\n         }\n         return groupMembers;\n     }\n}"
  },
  {
    "path": "src/websocket/handler/getMinioUploadUrlHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GMURL } from \"../../constant\";\n\nexport default class GetMinioUploadUrlHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GMURL;\n    }\n    notifyContent(content){\n        var result = JSON.parse(content);\n        return result;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/getUploadtokenHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GQNUT } from \"../../constant\";\nimport LocalStore from \"../store/localstore\";\n\nexport default class UploadTokenHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GQNUT;\n    }\n\n    processMessage(proto){\n        if(proto.content){\n            var uploadTokenResponse = JSON.parse(proto.content);\n            var domain = uploadTokenResponse.domain;\n            var token = uploadTokenResponse.token;\n            console.log(\"domain \"+domain+\" token \"+token);\n            LocalStore.setUploadToken(domain,token);\n        }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/getfriendresultHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, FP } from \"../../constant\";\nimport LocalStore from \"../store/localstore\";\n\nexport default class GetFriendResultHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == FP;\n    }\n\n    processMessage(proto){\n        var friendList  = JSON.parse(proto.content);\n        var userIds = [];\n        for(var i in friendList){\n            userIds[i] = friendList[i].friendUid;\n        }\n        this.vueWebsocket.sendAction(\"updateFriendIds\",friendList);\n        //获取当前登录用户信息\n        userIds.push(LocalStore.getUserId());\n        this.vueWebsocket.getUserInfos(userIds);\n    }\n}"
  },
  {
    "path": "src/websocket/handler/getuserinfoHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, UPUI, ERROR_CODE, SUCCESS_CODE } from \"../../constant\";\nimport UserInfo from \"../model/userInfo\";\nimport py from \"pinyin\"\nimport { isBuffer } from \"util\";\nimport FutureResult from \"../future/futureResult\";\n\nexport default class GetUserInfoHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == UPUI;\n    }\n\n    processMessage(proto){\n       if(proto.content != null && proto.content != ''){\n           var userInfoList = JSON.parse(proto.content);\n           var stateFriendList = [];\n           var userInfos = [];\n           for(var i in userInfoList){\n               var displayName = userInfoList[i].displayName === '' ? userInfoList[i].mobile : userInfoList[i].displayName;\n               var pinyinInitial = py(displayName,{\n                   style: py.STYLE_FIRST_LETTER\n               });\n               var initial = pinyinInitial[0][0];\n               \n               if(initial.length > 1){\n                   var initial = initial.substr(0,1);\n                   var reg= /^[A-Za-z]/;\n                   if(reg.test(initial)){\n                     initial = initial.toUpperCase();\n                   } else {\n                    initial = \"#\";\n                   }\n               } else {\n                 initial = initial.toUpperCase();\n               }\n               stateFriendList.push({\n                id: parseInt(i) + 1,\n                wxid: userInfoList[i].uid, //微信号\n                initial: initial, //姓名首字母\n                img: userInfoList[i].portrait, //头像\n                signature: \"\", //个性签名\n                nickname: displayName,  //昵称\n                sex: 0,   //性别 1为男，0为女\n                remark: displayName,  //备注\n                area: userInfoList[i].address,  //地区\n               });\n               userInfos.push(UserInfo.convert2UserInfo(userInfoList[i]));\n           }\n           if(this.vueWebsocket.resolvePromiseMap.has(proto.messageId)){\n               console.log(\"messageId \"+proto.messageId);\n              var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId);\n              clearTimeout(promiseReslove.timeoutId);\n              var displayName = userInfoList[0].displayName === '' ? userInfoList[0].mobile : userInfoList[0].displayName;\n              promiseReslove.resolve(new FutureResult(SUCCESS_CODE,displayName));\n              this.vueWebsocket.resolvePromiseMap.delete(proto.messageId);\n           }\n        \n           this.vueWebsocket.sendAction(\"updateUserInfos\",userInfos);\n           this.vueWebsocket.sendAction(\"updateFriendList\",stateFriendList);\n       }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/handleFriendRequestHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, FHR } from \"../../constant\";\nimport Logger from \"../utils/logger\";\n\nexport default class HandleFriendRequestHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == FHR;\n    }\n\n    processMessage(proto){\n        if(proto.content && proto.content != ''){\n           var message = JSON.parse(proto.content);\n           Logger.log(\"handle friend request \"+message);\n        }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/kickGroupmemberHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GKM } from \"../../constant\";\n\nexport default class KickGroupMemberHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GKM;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/loadRemoteMessageHander.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, LRM } from \"../../constant\";\n\nexport default class LoadRemoteMessageHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == LRM;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/messageHandler.js",
    "content": "export default class MessageHandler{\n    match(signal){\n        return false;\n    }\n\n    processMessage(data){\n\n    }\n}"
  },
  {
    "path": "src/websocket/handler/modifyMyInfoHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { MMI, PUB_ACK } from \"../../constant\";\nimport Logger from \"../utils/logger\";\n\nexport default class ModifyInfoHandler extends AbstractMessageHandler{\n   match(proto){\n    return proto.signal == PUB_ACK && proto.subSignal == MMI;\n   }\n\n   processMessage(proto){\n       if(proto && proto.content != ''){\n          Logger.log(\"modify myInfo \"+proto.content);\n       }\n   }\n}"
  },
  {
    "path": "src/websocket/handler/notifyFriendHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { FN, PUBLISH } from \"../../constant\";\nimport Logger from \"../utils/logger\";\n\nexport default class NotifyFriendHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUBLISH && proto.subSignal == FN;\n    }\n\n    processMessage(proto){\n        if(proto.content && proto.content != ''){\n            var friendNotify = JSON.parse(proto.content);\n            Logger.log(\"friend head \"+friendNotify.head);\n            this.vueWebsocket.getFriend();\n        }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/notifyFriendRequestHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUBLISH, FRN } from \"../../constant\";\nimport Logger from \"../utils/logger\";\nimport LocalStore from \"../store/localstore\";\n\nexport default class NotifyFriendRequestHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUBLISH && proto.subSignal == FRN;\n    }\n\n\n    processMessage(proto){\n        if(proto.content != null){\n           var friendRequestNotification = JSON.parse(proto.content);\n           Logger.log(\"friend request version \"+friendRequestNotification.version);\n           LocalStore.setFriendRequestVersion(friendRequestNotification.version);\n           this.vueWebsocket.getFriendRequest(friendRequestNotification.version);\n        }\n    }\n}"
  },
  {
    "path": "src/websocket/handler/notifyMessageHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUBLISH, MN } from \"../../constant\";\nimport LocalStore from \"../store/localstore\";\n\nexport default class NotifyMessageHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUBLISH && proto.subSignal == MN;\n    }\n\n    processMessage(proto){\n        console.log(\"notifymessage \"+proto.content);\n        let notify = JSON.parse(proto.content);\n        console.log(\"notify messageHead \"+notify.messageHead+\" notify type \"+notify.type);\n        //更新messageHead\n        LocalStore.resetSendMessageCount();\n        LocalStore.setLastMessageSeq(notify.messageHead);\n        this.vueWebsocket.pullMessage(notify.messageHead,notify.type,1);\n    }\n}"
  },
  {
    "path": "src/websocket/handler/notifyRecallMessageHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUBLISH, RMN } from \"../../constant\";\n\nexport default class NotifyRecallMessageHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUBLISH && proto.subSignal == RMN;\n    }\n\n    processMessage(proto){\n       if(proto.content){\n            var notifyRecalMessage = JSON.parse(proto.content);\n            console.log(\"from user \"+notifyRecalMessage.fromUser + \" messageUid \"+notifyRecalMessage.messageUid);\n            this.vueWebsocket.sendAction('updateMessageContent',notifyRecalMessage);\n       }\n    }\n\n}"
  },
  {
    "path": "src/websocket/handler/quitGroupHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, GQ } from \"../../constant\";\n\nexport default class QuitGroupHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == GQ;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/recallMessageHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, MR } from \"../../constant\";\n\nexport default class RecallMessageHandler extends AbstractMessageHandler {\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == MR;\n    }\n\n    notifyContent(content){\n        var content = JSON.parse(content);\n        return content.code;\n    }\n}"
  },
  {
    "path": "src/websocket/handler/receiveMessageHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { MP, PUB_ACK } from \"../../constant\";\nimport Message from \"../message/message\";\nimport ProtoMessage from \"../message/protomessage\";\nimport ProtoConversationInfo from '../model/protoConversationInfo'\nimport LocalStore from \"../store/localstore\";\nimport UnreadCount from \"../model/unReadCount\";\nimport MessageConfig from \"../message/messageConfig\";\nimport PersistFlag from \"../message/persistFlag\";\nimport ChatManager from \"../chatManager\";\n\nexport default class ReceiveMessageHandler extends AbstractMessageHandler {\n      conversations = [];\n\n      match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == MP;\n      }\n\n      processMessage(proto){\n        LocalStore.resetSendMessageCount();\n        var content = JSON.parse(proto.content);\n        console.log(\"current \"+content.current+\" head \"+content.head+\" messageCount \"+content.messageCount);\n        if(content.messageCount == 0){\n          this.vueWebsocket.sendAction('changeEmptyMessageState',true);\n        } else {\n          this.vueWebsocket.sendAction('changeEmptyMessageState',false);\n        }\n        for(var protoMessage of content.messageResponseList){\n          var protoMessage = ProtoMessage.toProtoMessage(protoMessage);\n          if(MessageConfig.isDisplayableMessage(protoMessage)){\n            this.addProtoMessage(protoMessage);\n          }\n          ChatManager.onReceiveMessage(protoMessage);\n        } \n        this.vueWebsocket.sendAction('changetFirstLogin',false);     \n      }\n\n      addProtoMessage(protoMessage){\n         this.vueWebsocket.sendAction('addProtoMessage',protoMessage);\n      }\n}"
  },
  {
    "path": "src/websocket/handler/searchUserResultHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { US,PUB_ACK } from \"../../constant\";\nimport Logger from \"../utils/logger\";\nimport UserInfo from \"../model/userInfo\";\n\nexport default class SearchUserResultHandler extends AbstractMessageHandler{\n   match(proto){\n     return proto.signal == PUB_ACK && proto.subSignal == US;\n   }\n\n   processMessage(proto){\n     var searchUserList = JSON.parse(proto.content);\n     this.vueWebsocket.sendAction(\"updateSearchUser\",searchUserList);\n   }\n}"
  },
  {
    "path": "src/websocket/handler/sendMessageHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { MS, PUB_ACK } from \"../../constant\";\nimport LocalStore from \"../store/localstore\";\nimport Logger from \"../utils/logger\";\nimport MessageStatus from \"../message/messageStatus\";\n\nexport default class SendMessageHandler extends AbstractMessageHandler{\n\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == MS;\n    }\n\n    processMessage(proto){\n        Logger.log(\"sendMessage Handler messageId \"+proto.messageId)\n        if(this.vueWebsocket.resolvePromiseMap.has(proto.messageId)){\n            var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId);\n            if(proto.content != ''){\n                var content = JSON.parse(proto.content);\n                console.log(\"SendMessageHandler messageId \"+content.messageId+\" timestamp protoMessageId \"+promiseReslove.protoMessageId);\n                this.vueWebsocket.sendAction(\"updateProtoMessageUid\",{\n                    messageId: promiseReslove.protoMessageId,\n                    messageUid: content.messageId\n                });\n                //update messageStatus\n                this.vueWebsocket.sendAction(\"updateMessageStatus\",{\n                    messageId: promiseReslove.protoMessageId,\n                    status: MessageStatus.Sent\n                });\n                LocalStore.updateSendMessageCount();\n                promiseReslove.resolve('success');\n            } else {\n                this.vueWebsocket.sendAction(\"updateMessageStatus\",{\n                    messageId: promiseReslove.protoMessageId,\n                    status: MessageStatus.SendFailure\n                });\n                promiseReslove.resolve('fail');\n            }\n            clearTimeout(promiseReslove.timeoutId);\n            this.vueWebsocket.resolvePromiseMap.delete(proto.messageId);\n        }\n\n    }\n}"
  },
  {
    "path": "src/websocket/handler/setFriendAliasRequestHandler.js",
    "content": "import AbstractMessageHandler from \"./abstractmessagehandler\";\nimport { PUB_ACK, FALS } from \"../../constant\";\n\nexport default class SetFriendAliasRequestHandler extends AbstractMessageHandler{\n    match(proto){\n        return proto.signal == PUB_ACK && proto.subSignal == FALS;\n    }\n\n}"
  },
  {
    "path": "src/websocket/index.js",
    "content": "import {PUBLISH, FP, UPUI, MP, MS, KEY_VUE_USER_ID, KEY_VUE_DEVICE_ID, DISCONNECT, GPGI, GQNUT, US, FAR, FRP, FHR, MMI, GPGM, GC, GQ, PUB_ACK, ERROR_CODE, MR, GAM, GMI, GKM, GD, GMURL, FALS, LRM} from '../constant'\nimport {decrypt,encrypt} from './utils/aes'\nimport {CONNECT} from '../constant'\nimport {WebSocketProtoMessage} from './message/websocketprotomessage'\nimport ConnectAckHandler from './handler/connectackhandler';\nimport GetFriendResultHandler from './handler/getfriendresultHandler';\nimport GetUserInfoHandler from './handler/getuserinfoHandler';\nimport ReceiveMessageHandler from './handler/receiveMessageHandler';\nimport NotifyMessageHandler from './handler/notifyMessageHandler';\nimport GetGroupInfoHandler from './handler/getGroupInfoHandler';\nimport SendMessageHandler from './handler/sendMessageHandler';\nimport UploadTokenHandler from './handler/getUploadtokenHandler'\nimport LocalStore from './store/localstore';\nimport SearchUserResultHandler from './handler/searchUserResultHandler';\nimport FriendAddRequestHandler from './handler/friendAddRequestHandler';\nimport NotifyFriendRequestHandler from './handler/notifyFriendRequestHandler';\nimport FriendRequestHandler from './handler/friendRequestHandler';\nimport HandleFriendRequestHandler from './handler/handleFriendRequestHandler';\nimport NotifyFriendHandler from './handler/notifyFriendHandler';\nimport ModifyInfoHandler from './handler/modifyMyInfoHandler';\nimport GetGroupMemberHandler from './handler/getGroupMemberHandler';\nimport PromiseResolve from './future/promiseResolve';\nimport {WS_PROTOCOL,WS_IP,WS_PORT,HEART_BEAT_INTERVAL,RECONNECT_INTERVAL,BINTRAY_TYPE} from '../constant/index'\nimport vuexStore from '../store'\nimport Logger from './utils/logger';\nimport GroupInfo from './model/groupInfo';\nimport GroupType from './model/groupType';\nimport GroupMember from './model/groupMember';\nimport GroupMemberType from './model/groupMemberType';\nimport CreateGroupHandler from './handler/createGroupHandler';\nimport QuitGroupHandler from './handler/quitGroupHandler';\nimport { fail } from 'assert';\nimport FutureResult from './future/futureResult';\nimport RecallMessageHandler from './handler/recallMessageHandler';\nimport NotifyRecallMessageHandler from './handler/notifyRecallMessageHandler';\nimport AddGroupMemberHandler from './handler/addGroupMemberHandler';\nimport KickGroupMemberHandler from './handler/kickGroupmemberHandler'\nimport DismissGroupHandler from './handler/dismissGroupHandler';\nimport GetMinioUploadUrlHandler from './handler/getMinioUploadUrlHandler';\nimport SetFriendAliasRequestHandler from './handler/setFriendAliasRequestHandler';\nimport LoadRemoteMessageHandler from './handler/loadRemoteMessageHander';\nexport default class VueWebSocket {\n    handlerList = [];\n    userDisconnect = false;\n    isconnected = false;\n    resolvePromiseMap = new Map();\n    \n    // constructor(ws_protocol,ip,port,heartbeatTimeout,reconnectInterval,binaryType,vuexStore){\n    //     this.ws_protocol = ws_protocol;\n    //     this.ip= ip;\n    //     this.port = port;\n    //     this.heartbeatTimeout = heartbeatTimeout;\n    //     this.reconnectInterval = reconnectInterval;\n    //     this.binaryType = binaryType;\n    //     this.url = ws_protocol + '://' + ip + ':'+ port;\n    //     this.vuexStore = vuexStore;\n    //     this.initHandlerList();\n    // }\n\n    constructor(){\n        this.ws_protocol = WS_PROTOCOL;\n        this.ip= WS_IP;\n        this.port = WS_PORT;\n        this.heartbeatTimeout = HEART_BEAT_INTERVAL;\n        this.reconnectInterval = RECONNECT_INTERVAL;\n        this.binaryType = BINTRAY_TYPE;\n        this.url = WS_PROTOCOL + '://' + WS_IP ;\n        this.initHandlerList();\n        this.connect(true);\n    }\n\n    // constructor(ws_protocol,ip,port,heartbeatTimeout,reconnectInterval,binaryType){\n    //     this.ws_protocol = ws_protocol;\n    //     this.ip= ip;\n    //     this.port = port;\n    //     this.heartbeatTimeout = heartbeatTimeout;\n    //     this.reconnectInterval = reconnectInterval;\n    //     this.binaryType = binaryType;\n    //     this.url = ws_protocol + '://' + ip + ':'+ port;\n    //     this.initHandlerList();\n    // }\n\n    connect(isReconncect){\n        this.ws = new WebSocket(this.url);\n        console.log(\"current url \"+this.url+\" status \"+this.ws.readyState);\n        this.ws.binaryType = this.binaryType;\n        var websocketObj = this;\n        this.ws.onopen = function (event) {\n            console.log(\"ws open\");\n            websocketObj.isconnected = true;\n            websocketObj.lastInteractionTime(new Date().getTime());\n            websocketObj.pingIntervalId = setInterval(() => {\n                 websocketObj.ping();\n             }, websocketObj.heartbeatTimeout);\n             websocketObj.userDisconnect = false;\n             //发送connect指令\n             websocketObj.sendConnectMessage();\n        }\n        this.ws.onmessage = function(event) {\n            console.log(\"ws onmessage[\"+event.data+\"]\");\n            websocketObj.processMessage(event.data);\n            websocketObj.lastInteractionTime(new Date().getTime());\n        }\n        this.ws.onclose = function(event) {\n            websocketObj.isconnected = false;\n            console.log(\"ws onclose\");\n            websocketObj.ws.close();\n            clearInterval(websocketObj.pingIntervalId);\n            if(!websocketObj.userDisconnect){\n                console.log(\"reconnect websocket\");\n                websocketObj.reconnect(event);\n            }\n        }\n        this.ws.onerror = function(event) {\n            console.log(\"connect error\");\n        }\n    }\n\n    reconnect(event){\n        var websocketObj = this;\n        setTimeout(() => {\n            websocketObj.connect(true);\n        }, this.reconnectInterval);\n    }\n\n    lastInteractionTime(actionTime){\n        this.actionTime = actionTime;\n    }\n\n    getLastActionTime(){\n        return this.actionTime;\n    }\n\n    ping(){\n        this.send('心跳内容')\n    }\n\n    send(data){\n        console.log(\"send message \"+data);\n        if(this.isconnected){\n            this.ws.send(data);\n        } else {\n            console.log(\"curent websocket is close\");\n        }\n        \n    }\n\n    /**\n     * 分发vuex action\n     */\n    sendAction(type,data){\n        vuexStore.dispatch(type,data);\n    }\n\n    initHandlerList(){\n        this.handlerList.push(new ConnectAckHandler(this));\n        this.handlerList.push(new GetFriendResultHandler(this));\n        this.handlerList.push(new GetUserInfoHandler(this));\n        this.handlerList.push(new ReceiveMessageHandler(this));\n        this.handlerList.push(new NotifyMessageHandler(this));\n        this.handlerList.push(new GetGroupInfoHandler(this));\n        this.handlerList.push(new SendMessageHandler(this));\n        this.handlerList.push(new UploadTokenHandler(this));\n        this.handlerList.push(new SearchUserResultHandler(this));\n        this.handlerList.push(new FriendAddRequestHandler(this));\n        this.handlerList.push(new NotifyFriendRequestHandler(this));\n        this.handlerList.push(new FriendRequestHandler(this));\n        this.handlerList.push(new HandleFriendRequestHandler(this));\n        this.handlerList.push(new NotifyFriendHandler(this));\n        this.handlerList.push(new ModifyInfoHandler(this));\n        this.handlerList.push(new GetGroupMemberHandler(this));\n        this.handlerList.push(new CreateGroupHandler(this));\n        this.handlerList.push(new QuitGroupHandler(this));\n        this.handlerList.push(new RecallMessageHandler(this));\n        this.handlerList.push(new NotifyRecallMessageHandler(this));\n        this.handlerList.push(new AddGroupMemberHandler(this));\n        this.handlerList.push(new KickGroupMemberHandler(this));\n        this.handlerList.push(new DismissGroupHandler(this));\n        this.handlerList.push(new GetMinioUploadUrlHandler(this))\n        this.handlerList.push(new SetFriendAliasRequestHandler(this))\n        this.handlerList.push(new LoadRemoteMessageHandler(this))\n    }\n\n    processMessage(data){\n        var protoObj = JSON.parse(data);\n        for(var i = 0; i < this.handlerList.length; i++){\n            if(this.handlerList[i].match(protoObj)){\n                 this.handlerList[i].processMessage(protoObj);\n            }\n        }\n    }\n\n    /**\n     * 链接建立信息\n     */\n    sendConnectMessage(){\n        console.log(\"userToken \"+localStorage.getItem('vue-token'));\n        let allToken = decrypt(localStorage.getItem('vue-token'));\n        console.log(\"decryptToken \"+allToken);\n        let pwd = allToken.substring(0,allToken.indexOf('|'));\n        allToken = allToken.substring(allToken.indexOf('|')+1);\n        let secret = allToken.substring(0,allToken.indexOf('|'));\n        console.log('[pwd]->'+pwd+' [secret]->'+secret);\n        let pwdAesBase64 = encrypt(pwd,secret);\n        console.log('encrypt pwd: '+pwdAesBase64);\n\n        var websocketprotomessage =  new WebSocketProtoMessage();\n        websocketprotomessage.setSignal(CONNECT);\n        var connectMessage = {\n            userName: LocalStore.getUserId(),\n            password: pwdAesBase64,\n            clientIdentifier: localStorage.getItem(KEY_VUE_DEVICE_ID)\n        }\n        websocketprotomessage.content = connectMessage;\n        console.log(websocketprotomessage.toJson());\n        this.send(websocketprotomessage.toJson());\n    }\n\n\n    sendDisConnectMessage(){\n        var websocketprotomessage = new WebSocketProtoMessage();\n        websocketprotomessage.setSignal(DISCONNECT);\n        var disconnectMessage = {\n            clearSession : 1\n        }\n        websocketprotomessage.content = disconnectMessage;\n        console.log(websocketprotomessage.toJson());\n        this.send(websocketprotomessage.toJson());\n        this.userDisconnect = true;\n    }\n\n    /**\n     * 获取朋友列表\n     */\n    getFriend(version = 0){\n        this.sendPublishMessage(FP,{version : version});\n    }\n\n    searchUser(keyword){\n        var content = {\n            keyword: keyword,\n            fuzzy: 1,\n            page: 0\n        }\n        this.sendPublishMessage(US,content);\n    }\n\n    sendFriendAddRequest(value){\n        this.sendPublishMessage(FAR,value);\n    }\n\n    getFriendRequest(version){\n        this.sendPublishMessage(FRP,{\n            version: version\n        });\n    }\n\n    handleFriendRequest(value){\n        this.sendPublishMessage(FHR,value);\n    }\n    \n    /**\n     * 获取用户详细信息\n     */\n    getUserInfos(userIds){\n        this.sendPublishMessage(UPUI,userIds);\n    }\n\n    async getUserInfo(userId){\n        var promise = await this.sendPublishMessage(UPUI,[userId]);\n        return promise\n    }\n\n    /**\n     * \n     * @param {用户信息} info: {\n     * type:  0\n     * value: \n     * }\n     */\n    modifyMyInfo(info){\n        this.sendPublishMessage(MMI,info);\n    }\n\n    /**\n     * \n     * @param {群组id} groupId \n     * @param {是否需要刷新} refresh \n     */\n    getGroupInfo(groupId,refresh){\n        var groupIds = [];\n        groupIds.push(groupId);\n        this.sendPublishMessage(GPGI,groupIds);\n    }\n\n    async getGroupMember(groupId,refresh){\n       return await this.sendPublishMessage(GPGM,{\n           groupId: groupId,\n           version: 0\n       })\n    }\n\n    async addMembers(groupId,memberIds){\n       var groupMembers = [];\n       for(var memberId of memberIds){\n            var groupMember = new GroupMember();\n            groupMember.memberId = memberId;\n            groupMember.type = GroupMemberType.Normal;\n            groupMembers.push(groupMember);\n       }\n       return await this.sendPublishMessage(GAM,{\n           groupId: groupId,\n           groupMembers: groupMembers\n       });\n    }\n\n    async kickeMembers(groupId,memberIds){\n       return await this.sendPublishMessage(GKM,{\n           groupId: groupId,\n           memberIds: memberIds\n       });\n    }\n\n    async createGroup(groupName,memberIds){\n        var groupInfo = new GroupInfo();\n        groupInfo.name = groupName;\n        groupInfo.type = GroupType.Normal;\n        var groupMembers = [];\n        for(var memberId of memberIds){\n            var groupMember = new GroupMember();\n            groupMember.memberId = memberId;\n            groupMember.type = memberId == LocalStore.getUserId() ? GroupMemberType.Owner : GroupMemberType.Normal;\n            groupMembers.push(groupMember);\n        }\n        return await this.sendPublishMessage(GC,{\n            groupInfo: groupInfo,\n            groupMembers: groupMembers\n        });\n    }\n\n    async modifyGroupInfo(info){\n        return await this.sendPublishMessage(GMI,info);\n    }\n\n    async quitGroup(groupId){\n       return await this.sendPublishMessage(GQ,{\n           groupId: groupId\n       });\n    }\n\n    async dismissGroup(groupId){\n        return await this.sendPublishMessage(GD,{\n            groupId: groupId\n        });\n    }\n\n    async recallMessage(messageUid){\n       return await this.sendPublishMessage(MR,{\n            messageUid: messageUid\n       })\n    }\n\n    async getRemoteMessages(conversation,beforeUid,count){\n        return await this.sendPublishMessage(LRM,{\n            beforeUid: beforeUid,\n            count: count,\n            conversation: conversation\n        })\n    }\n\n    pullMessage(messageId,type = 0,pullType = 0,sendMessageCount = 0){\n        this.sendPublishMessage(MP,{\n            messageId: messageId,\n            type: type,\n            pullType : pullType,\n            sendMessageCount: sendMessageCount\n        });\n    }\n\n    getUploadToken(mediaType){\n       var content = {\n        mediaType: mediaType\n       };\n       this.sendPublishMessage(GQNUT,content);\n    }\n\n    async getMinioUploadUrl(mediaType,key){\n        var content = {\n            mediaType: mediaType,\n            key: key\n        }\n        return await this.sendPublishMessage(GMURL,content)\n    }\n\n    async modifyFriendAlias(targetUid,alias){\n        var content = {\n            reason: alias,\n            targetUserId: targetUid\n        }\n        return await this.sendPublishMessage(FALS,content)\n    }\n\n    /**\n     * \n     * @param {子信令} subsignal \n     * @param {消息体内容} content \n     */\n    sendPublishMessage(subsignal,content,protoMessageId = 0){\n        var websocketprotomessage = new WebSocketProtoMessage();\n        websocketprotomessage.setSignal(PUBLISH);\n        websocketprotomessage.setSubSignal(subsignal);\n        websocketprotomessage.setContent(content);\n        var messageId = LocalStore.getMessageId();\n        if(messageId > 65535){\n            messageId = 0;\n        }\n        websocketprotomessage.setMessageId(++messageId);\n        LocalStore.saveMessageId(messageId);\n        this.send(websocketprotomessage.toJson());\n        \n        var vueWebSocket = this;\n        var pubAckPromise = new Promise((resolve) => {\n             var timeoutId = setTimeout(() => {\n                 if(subsignal == MS){\n                    var failProtoMessage = new WebSocketProtoMessage();\n                    failProtoMessage.setSignal(PUB_ACK);\n                    failProtoMessage.setSubSignal(subsignal)\n                    failProtoMessage.setMessageId(messageId);\n                    failProtoMessage.setContent('')\n                    vueWebSocket.processMessage(failProtoMessage.toJson())\n                 } else {\n                    resolve(new FutureResult(ERROR_CODE,\"\"));\n                 }\n                 \n             },10000);\n             var resolvePromise = new PromiseResolve(resolve,timeoutId);\n             resolvePromise.protoMessageId = protoMessageId;\n             vueWebSocket.resolvePromiseMap.set(messageId,resolvePromise);\n        });\n        return pubAckPromise;\n    }\n\n    sendMessage(protoMessage){\n       this.sendPublishMessage(MS,protoMessage,protoMessage.messageId);\n    }\n}"
  },
  {
    "path": "src/websocket/listener/onReceiverMessageListener.js",
    "content": "export default class OnReceiverMessageListener {\n    onReceiveMessage(protoMessage){\n\n    }\n}"
  },
  {
    "path": "src/websocket/message/fileMessageContent.js",
    "content": "import MediaMessageContent from './mediaMessageContent';\nimport MessageContentMediaType from './messageContentMediaType';\nimport MessageContentType from './messageContentType';\n\nexport default class FileMessageContent extends MediaMessageContent {\n    name = '';\n    size = 0;\n    static FILE_NAME_PREFIX = '[文件] ';\n\n    constructor(fileOrLocalPath, remotePath) {\n        super(MessageContentType.File, MessageContentMediaType.File, fileOrLocalPath, remotePath);\n        if (typeof File !== 'undefined' && fileOrLocalPath instanceof File) {\n            this.name = fileOrLocalPath.name;\n            this.size = fileOrLocalPath.size;\n        }\n    }\n\n    digest() {\n        return '[文件]';\n    }\n\n    encode() {\n        let payload = super.encode();\n        payload.searchableContent = FileMessageContent.FILE_NAME_PREFIX + this.name;\n        payload.content = this.size + '';\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        if(payload.searchableContent){\n            if(payload.searchableContent.indexOf(FileMessageContent.FILE_NAME_PREFIX) === 0){\n                this.name = payload.searchableContent.substring(payload.searchableContent.indexOf(FileMessageContent.FILE_NAME_PREFIX) + FileMessageContent.FILE_NAME_PREFIX.length);\n            }else {\n        this.name = payload.searchableContent;\n            }\n        this.size = this.formateSize(payload.content);\n        }\n    }\n\n    formateSize(value) { if (null == value || value == '') { return \"0 Bytes\"; }\n        var unitArr = new Array(\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"); var index = 0;\n        var srcsize = parseFloat(value); index = Math.floor(Math.log(srcsize) / Math.log(1024));\n        var size = srcsize / Math.pow(1024, index); size = size.toFixed(2);\t\t//保留的小数位数\n        return size + unitArr[index];\n    }\n\n}"
  },
  {
    "path": "src/websocket/message/imageMessageContent.js",
    "content": "import MediaMessageContent from './mediaMessageContent';\nimport MessageContentMediaType from './messageContentMediaType';\nimport MessageContentType from './messageContentType';\n\nexport default class ImageMessageContent extends MediaMessageContent {\n    // base64 encoded, 不包含头部:data:image/png;base64,\n    thumbnail;\n\n    constructor(fileOrLocalPath, remotePath, thumbnail) {\n        super(MessageContentType.Image, MessageContentMediaType.Image, fileOrLocalPath, remotePath);\n        this.thumbnail = thumbnail;\n    }\n\n    digest() {\n        return '[图片]';\n    }\n\n    encode() {\n        let payload = super.encode();\n        payload.mediaType = MessageContentMediaType.Image;\n        payload.binaryContent = this.thumbnail;\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        this.thumbnail = payload.binaryContent;\n    }\n}"
  },
  {
    "path": "src/websocket/message/mediaMessageContent.js",
    "content": "import MessageContent from './messageContent'\nexport default class MediaMessageContent extends MessageContent {\n    file;\n    remotePath = '';\n    localPath = '';\n    mediaType = 0;\n\n    constructor(messageType, mediaType = 0, fileOrLocalPath, remotePath) {\n        super(messageType);\n        this.mediaType = mediaType;\n        this.remotePath = remotePath;\n        if(typeof fileOrLocalPath === \"string\"){\n          this.localPath = fileOrLocalPath;\n        }else {\n          this.file = fileOrLocalPath;\n          if (fileOrLocalPath && fileOrLocalPath.path !== undefined) {\n            this.localPath = fileOrLocalPath.path;\n            // attention: 粘贴的时候，path是空字符串，故采用了这个trick\n            if (this.localPath.indexOf(fileOrLocalPath.name) < 0) {\n              this.localPath += fileOrLocalPath.name;\n            }\n          }\n        }\n    }\n\n    encode() {\n        let payload = super.encode();\n        payload.localMediaPath = this.localPath;\n        payload.remoteMediaUrl = this.remotePath;\n        payload.mediaType = this.mediaType;\n        payload.searchableContent = this.digest();\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        this.localPath = payload.localMediaPath;\n        this.remotePath = payload.remoteMediaUrl;\n        this.mediaType = payload.mediaType;\n    }\n}"
  },
  {
    "path": "src/websocket/message/message.js",
    "content": "import Conversation from '../model/conversation'\nimport MessageStatus from './messageStatus'\nimport store from '../../store'\nimport LocalStore from '../store/localstore';\n/***\n *  message in json format\n    {\n        \"conversation\":{\n            \"conversationType\": 0, \n            \"target\": \"UZUWUWuu\", \n            \"line\": 0, \n        }\n        \"from\": \"UZUWUWuu\", \n        \"content\": {\n            \"type\": 1, \n            \"searchableContent\": \"1234\", \n            \"pushContent\": \"\", \n            \"content\": \"\", \n            \"binaryContent\": \"\", \n            \"localContent\": \"\", \n            \"mediaType\": 0, \n            \"remoteMediaUrl\": \"\", \n            \"localMediaPath\": \"\", \n            \"mentionedType\": 0, \n            \"mentionedTargets\": [ ]\n        }, \n        \"messageId\": 52, \n        \"direction\": 1, \n        \"status\": 5, \n        \"messageUid\": 75735276990792720, \n        \"timestamp\": 1550849394256, \n        \"to\": \"\"\n    }\n */\nexport default class Message {\n    conversation = {};\n    from = '';\n    content = {}; \n    messageId = 0;\n    direction = 0;\n    status = 0;\n    messageUid = 0;\n    timestamp = 0;\n    tos = '';\n\n    // static toMessage(obj){\n    //     let msg = new Message();\n    //     msg.conversation = new Conversation(obj.conversationType,obj.target,obj.line);\n    //     msg.from = obj.from;\n    //     msg.content = obj.content;\n    //     msg.messageId = obj.messageId;\n    //     if(obj.from == USER_ID){\n    //         msg.direction = 0;\n    //     } else {\n    //         msg.direction = 1;\n    //     }\n    //     msg.status = obj.status;\n    //     msg.messageUid = obj.messageId;\n    //     msg.timestamp = obj.timestamp;\n    //     msg.to = obj.target;\n    //     return msg;\n    // }\n\n    static toMessage(state,sendMessage){\n        var message = new Message();\n        var target = sendMessage.target;\n        if(!target){\n            target = state.selectTarget;\n        }\n        if(sendMessage.tos != ''){\n           message.tos = sendMessage.tos;\n        }\n        console.log(\"to message target \"+target);\n        let stateConversationInfo =  state.conversations.find(conversation => conversation.conversationInfo.target === target);\n        console.log(\"conversationtype \"+stateConversationInfo.conversationInfo.conversationType +\" target \"+stateConversationInfo.conversationInfo.target);\n        message.conversation = new Conversation(stateConversationInfo.conversationInfo.conversationType,\n            stateConversationInfo.conversationInfo.target,\n            stateConversationInfo.conversationInfo.line);\n            console.log(\"send message content \"+sendMessage.messageContent)\n        message.content = sendMessage.messageContent;\n        message.from = state.userId;\n        message.status = MessageStatus.Sending;\n        message.timestamp = new Date().getTime();\n        message.direction = 0;\n        message.messageId = new Date().getTime(); \n        return message;\n    }\n\n    //方法不支持重载\n    static conert2Message(sendMessage){\n        var message = new Message();\n        var target = sendMessage.target;\n        if(!target){\n            target = store.state.selectTarget;\n        }\n        console.log(\"to message target \"+target);\n        let stateConversationInfo =  store.state.conversations.find(conversation => conversation.conversationInfo.target === target);\n        console.log(\"conversationtype \"+stateConversationInfo.conversationInfo.conversationType +\" target \"+stateConversationInfo.conversationInfo.target);\n        message.conversation = new Conversation(stateConversationInfo.conversationInfo.conversationType,\n            stateConversationInfo.conversationInfo.target,\n            stateConversationInfo.conversationInfo.line);\n        message.content = sendMessage.messageContent;\n        message.from = LocalStore.getUserId();\n        message.status = MessageStatus.Sending;\n        message.timestamp = new Date().getTime();\n        message.direction = 0;\n        message.messageId = new Date().getTime(); \n        return message;\n    }\n}"
  },
  {
    "path": "src/websocket/message/messageConfig.js",
    "content": "import PersistFlag  from './persistFlag'\nimport UnknownMessageContent from './unknownMessageContent'\nimport MessageContentType from './messageContentType'\nimport TextMessageContent  from './textMessageContent'\nimport UnsupportMessageContent from './unsupportMessageContent'\nimport CallStartMessageContent from '../../webrtc/message/callStartMessageContent'\nimport CallAnswerMessageContent from '../../webrtc/message/callAnswerMessageContent'\nimport CallAnswerTMessageContent from '../../webrtc/message/callAnswerTMessageContent'\nimport CallSignalMessageContent from '../../webrtc/message/callSignalMessageContent'\nimport ImageMessageContent from './imageMessageContent'\nimport CallByeMessageContent from '../../webrtc/message/callByeMessageContent'\nimport CallModifyMessageContent from '../../webrtc/message/callModifyMessageContent'\nimport CreateGroupNotification from './notification/createGroupNotification'\nimport ChangeGroupNameNotification from './notification/changeGroupNameNotification'\nimport NotificationMessageContent from './notification/notificationMessageContent'\nimport AddGroupMemberNotification from './notification/addGroupMemberNotification'\nimport QuitGroupNotification from './notification/quitGroupNotification'\nimport RecallMessageNotification from './notification/recallMessageNotification'\nimport KickoffGroupMemberNotification from './notification/kickoffGroupMemberNotification'\nimport DismissGroupNotification from './notification/dismissGroupNotification'\nimport LocalStore from '../store/localstore'\nimport VideoMessageContent from './videoMessageContent'\nimport FileMessageContent from './fileMessageContent'\nexport default class MessageConfig{\n    static getMessageContentClazz(type) {\n        for (const content of MessageConfig.MessageContents) {\n            if (content.type === type) {\n                if (content.contentClazz) {\n                    return content.contentClazz;\n                } else {\n                    return UnsupportMessageContent;\n                }\n            }\n        }\n        console.log(`message type ${type} is unknown`);\n        return UnknownMessageContent;\n    }\n\n    static convert2MessageContent(from,protoMessageContent){\n        var messageContent = null;\n        var contentClazz =  this.getMessageContentClazz(protoMessageContent.type);\n        if(contentClazz != UnsupportMessageContent){\n           let content = new contentClazz();\n           try {\n               content.decode(protoMessageContent);\n               if (content instanceof NotificationMessageContent) {\n                    content.fromSelf = from === LocalStore.getUserId();\n               }\n               messageContent = content;\n           }catch(error){\n               console.log(\"decode message content error \"+protoMessageContent);\n           }\n        } \n        return messageContent;\n    }\n\n\n    static getMessageContentPersitFlag(type) {\n        for (const content of MessageConfig.MessageContents) {\n            if (content.type === type) {\n                return content.flag;\n            }\n        }\n        return 0;\n    }\n\n    static isDisplayableMessage(protomessage){\n        var messageContent =  protomessage.content;\n        if(MessageConfig.getMessageContentPersitFlag(messageContent.type) == PersistFlag.Persist ||\n             MessageConfig.getMessageContentPersitFlag(messageContent.type) == PersistFlag.Persist_And_Count){\n           return true;\n        }\n        return false;\n     }\n\n\n    static MessageContents = [\n        {\n            name: 'unknown',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.Unknown,\n            contentClazz: UnknownMessageContent,\n        },\n        {\n            name: 'text',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.Text,\n            contentClazz: TextMessageContent,\n        },\n        {\n            name: 'ptext',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.P_Text,\n        },\n        {\n            name: 'voice',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.Voice,\n        },\n        {\n            name: 'image',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.Image,\n            contentClazz: ImageMessageContent,\n        },\n        {\n            name: 'location',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.Location,\n        },\n        {\n            name: 'file',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.File,\n            contentClazz: FileMessageContent,\n        },\n        {\n            name: 'video',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.Video,\n            contentClazz: VideoMessageContent\n        },\n        {\n            name: 'sticker',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.Sticker,\n        },\n        {\n            name: 'imageText',\n            flag: PersistFlag.Persist_And_Count,\n            type: MessageContentType.ImageText,\n        },\n        {\n            name: 'tip',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.Tip_Notification,\n        },\n        {\n            name: 'typing',\n            flag: PersistFlag.Transparent,\n            type: MessageContentType.Typing,\n        },\n        {\n            name: 'addGroupMemberNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.AddGroupMember_Notification,\n            contentClazz: AddGroupMemberNotification\n        },\n        {\n            name: 'changeGroupNameNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.ChangeGroupName_Notification,\n            contentClazz: ChangeGroupNameNotification\n        },\n        {\n            name: 'changeGroupPortraitNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.ChangeGroupPortrait_Notification,\n        },\n        {\n            name: 'createGroupNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.CreateGroup_Notification,\n            contentClazz: CreateGroupNotification\n        },\n        {\n            name: 'dismissGroupNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.DismissGroup_Notification,\n            contentClazz: DismissGroupNotification\n        },\n        {\n            name: 'kickoffGroupMemberNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.KickOffGroupMember_Notification,\n            contentClazz: KickoffGroupMemberNotification\n        },\n        {\n            name: 'modifyGroupAliasNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.ModifyGroupAlias_Notification,\n        },\n        {\n            name: 'quitGroupNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.QuitGroup_Notification,\n            contentClazz: QuitGroupNotification,\n        },\n        {\n            name: 'transferGroupOwnerNotification',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.TransferGroupOwner_Notification,\n        },\n        {\n            name: 'recall',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.RecallMessage_Notification,\n            contentClazz: RecallMessageNotification\n        },\n        {\n            name: 'callStartMessageContent',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.VOIP_CONTENT_TYPE_START,\n            contentClazz: CallStartMessageContent,\n        },\n        {\n            name: 'callAnswerMessageContent',\n            flag: PersistFlag.No_Persist,\n            type: MessageContentType.VOIP_CONTENT_TYPE_ACCEPT,\n            contentClazz: CallAnswerMessageContent,\n        },\n        {\n            name: 'callAnswerTMessageContent',\n            flag: PersistFlag.Transparent,\n            type: MessageContentType.VOIP_CONTENT_TYPE_ACCEPT_T,\n            contentClazz: CallAnswerTMessageContent,\n        },\n        {\n            name: 'callByeMessageContent',\n            flag: PersistFlag.No_Persist,\n            type: MessageContentType.VOIP_CONTENT_TYPE_END,\n            contentClazz: CallByeMessageContent,\n        },\n        {\n            name: 'callSignalMessageContent',\n            flag: PersistFlag.Transparent,\n            type: MessageContentType.VOIP_CONTENT_TYPE_SIGNAL,\n            contentClazz: CallSignalMessageContent\n        },\n        {\n            name: 'callModifyMessageContent',\n            flag: PersistFlag.No_Persist,\n            type: MessageContentType.VOIP_CONTENT_TYPE_MODIFY,\n            contentClazz: CallModifyMessageContent,\n        },\n        {\n            name: 'callAddParticipant',\n            flag: PersistFlag.Persist,\n            type: MessageContentType.VOIP_CONTENT_TYPE_ADD_PARTICIPANT,\n        },\n        {\n            name: 'callMuteVideo',\n            flag: PersistFlag.No_Persist,\n            type: MessageContentType.VOIP_CONTENT_TYPE_MUTE_VIDEO,\n        },\n    ];\n}"
  },
  {
    "path": "src/websocket/message/messageContent.js",
    "content": "import MessagePayload from \"./messagePayload\";\n\n/**\n * 消息类型基类，一般包括文本消息，文件类消息，媒体类消息\n */\nexport default class MessageContent {\n    type;\n    //0 普通消息, 1 部分提醒, 2 提醒全部\n    mentionedType = 0;\n    //提醒对象，mentionedType 1时有效\n    mentionedTargets = [];\n    extra;\n\n    constructor(type, mentionedType = 0, mentionedTargets = []) {\n        this.type = type;\n        this.mentionedType = mentionedType;\n        this.mentionedTargets = mentionedTargets;\n    }\n\n    digest() {\n        return '...digest...';\n    }\n\n    /**\n     * return MessagePayload in json format\n     */\n    encode() {\n        let payload = new MessagePayload();\n        payload.type = this.type;\n        payload.mentionedType = this.mentionedType;\n        payload.mentionedTargets = this.mentionedTargets;\n        return payload;\n    }\n\n    /**\n     * \n     * @param {object} payload object json.parse from message#content \n     */\n    decode(payload) {\n        this.type = payload.type;\n        this.mentionedType = payload.mentionedType;\n        this.mentionedTargets = payload.mentionedTargets;\n    }\n}"
  },
  {
    "path": "src/websocket/message/messageContentMediaType.js",
    "content": "export default class MessageContentMediaType {\n    static General = 0;\n    static Image = 1;\n    static Voice = 2;\n    static Video = 3;\n    static File = 4;\n    static Portrait = 5;\n    static Fvaorite = 6;\n}"
  },
  {
    "path": "src/websocket/message/messageContentType.js",
    "content": "export default class MessageContentType {\n    // 基本消息类型\n    static Unknown = 0;\n    static Text = 1;\n    static Voice = 2;\n    static Image = 3;\n    static Location = 4;\n    static File = 5;\n    static Video = 6;\n    static Sticker = 7;\n    static ImageText = 8;\n    static P_Text = 9;\n\n    // 提醒消息\n    static RecallMessage_Notification = 80;\n    static Tip_Notification = 90;\n    static Typing = 91;\n\n    // 群相关消息\n    static CreateGroup_Notification = 104;\n    static AddGroupMember_Notification = 105;\n    static KickOffGroupMember_Notification = 106;\n    static QuitGroup_Notification = 107;\n    static DismissGroup_Notification = 108;\n    static TransferGroupOwner_Notification = 109;\n    static ChangeGroupName_Notification = 110;\n    static ModifyGroupAlias_Notification = 111;\n    static ChangeGroupPortrait_Notification = 112;\n\n    static MuteGroupMember_Notification = 113;\n    static ChangeJoinType_Notification = 114;\n    static ChangePrivateChat_Notification = 115;\n    static ChangeSearchable_Notificaiton = 116;\n    static SetGroupManager_Notification = 117;\n    static VOIP_CONTENT_TYPE_START = 400;\n    static VOIP_CONTENT_TYPE_END = 402;\n    static VOIP_CONTENT_TYPE_ACCEPT = 401;\n    static VOIP_CONTENT_TYPE_SIGNAL = 403;\n    static VOIP_CONTENT_TYPE_MODIFY = 404;\n    static VOIP_CONTENT_TYPE_ACCEPT_T = 405;\n}"
  },
  {
    "path": "src/websocket/message/messagePayload.js",
    "content": "/**\n * \n        \"content\": {\n            \"type\": 1, \n            \"searchableContent\": \"1234\", \n            \"pushContent\": \"\", \n            \"content\": \"\", \n            \"binaryContent\": \"\", \n            \"localContent\": \"\", \n            \"mediaType\": 0, \n            \"remoteMediaUrl\": \"\", \n            \"localMediaPath\": \"\", \n            \"mentionedType\": 0, \n            \"mentionedTargets\": [ ]\n        }, \n */\nexport default class MessagePayload {\n    type;\n    searchableContent;\n    pushContent;\n    content;\n    binaryContent; // base64 string, 图片时，不包含头部信息:data:image/png;base64,\n    localContent;\n    mediaType;\n    remoteMediaUrl;\n    localMediaPath;\n    mentionedType = 0;\n    mentionedTargets = [];\n    extra;\n}"
  },
  {
    "path": "src/websocket/message/messageStatus.js",
    "content": "export default class MessageStatus {\n    static Sending = 0;\n    static Sent = 1;\n    static SendFailure = 2;\n    static Mentioned = 3;\n    static AllMentioned = 4;\n    static Unread = 5;\n    static Readed = 6;\n    static Played = 7;\n}"
  },
  {
    "path": "src/websocket/message/modifyGroupInfoType.js",
    "content": "export default class ModifyGroupInfoType {\n    static Modify_Group_Name = 0;\n    static Modify_Group_Portrait = 1;\n    static Modify_Group_Extra = 2;\n}"
  },
  {
    "path": "src/websocket/message/myInfoType.js",
    "content": "export default class MyInfotype {\n    static Modify_DisplayName = 0;\n    static Modify_Portrait = 1;\n    static Modify_Gender = 2;\n    static Modify_Mobile = 3;\n    static Modify_Email = 4;\n    static Modify_Address = 5;\n    static Modify_Company = 6;\n    static Modify_Social = 7;\n    static Modify_Extra = 8;\n}"
  },
  {
    "path": "src/websocket/message/notification/addGroupMemberNotification.js",
    "content": "import MessageContentType from '../messageContentType';\n\nimport GroupNotificationContent from './groupNotification';\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class AddGroupMemberNotification extends GroupNotificationContent {\n    invitor = '';\n    invitees = [];\n\n    constructor(invitor, invitees) {\n        super(MessageContentType.AddGroupMember_Notification);\n        this.invitor = invitor;\n        this.invitees = invitees;\n    }\n\n    formatNotification() {\n        let notifyStr;\n        if (this.invitees.length === 1 && this.invitees[0] === this.invitor) {\n          if (this.fromSelf) {\n              return '您加入了群组';\n          } else {\n              return webSocketCli.getDisplayName(this.invitor) + ' 加入了群组';\n          }\n        }\n\n        if (this.fromSelf) {\n            notifyStr = '您邀请:';\n        } else {\n            notifyStr = webSocketCli.getDisplayName(this.invitor) + '邀请:';\n        }\n\n        let membersStr = '';\n        this.invitees.forEach(m => {\n            membersStr += ' ' + webSocketCli.getDisplayName(m);\n        });\n\n        return notifyStr + membersStr + '加入了群组';\n    }\n\n    encode() {\n        let payload = super.encode();\n        let obj = {\n            g: this.groupId,\n            o: this.invitor,\n            ms: this.invitees,\n        };\n        payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        let json = StringUtils.b64_to_utf8(payload.binaryContent);\n        let obj = JSON.parse(json);\n        this.groupId = obj.g;\n        this.invitor = obj.o;\n        this.invitees = obj.ms;\n    }\n}"
  },
  {
    "path": "src/websocket/message/notification/changeGroupNameNotification.js",
    "content": "import MessageContentType from \"../messageContentType\";\n\nimport GroupNotificationContent from \"./groupNotification\";\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class ChangeGroupNameNotification extends GroupNotificationContent {\n    operator = '';\n    name = '';\n\n    constructor(operator, name) {\n        super(MessageContentType.ChangeGroupName_Notification);\n        this.operator = operator;               \n        this.name = name;\n    }\n\n    formatNotification() {\n        if (this.fromSelf) {\n            return '您修改群名称为：' + this.name;\n        } else {\n            return webSocketCli.getDisplayName(this.operator)+' 修改群名称为:' + this.name;\n        }\n    }\n\n    encode() {\n        let payload = super.encode();\n        let obj = {\n            g: this.groupId,\n            n: this.name,\n            o: this.operator,\n        };\n        payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n        return payload;\n    }\n\n    decode(payload) {\n        super.decode(payload);\n        let json = StringUtils.b64_to_utf8(payload.binaryContent)\n        let obj = JSON.parse(json);\n        this.groupId = obj.g;\n        this.operator = obj.o;\n        this.name = obj.n;\n    }\n\n}"
  },
  {
    "path": "src/websocket/message/notification/createGroupNotification.js",
    "content": "import MessageContentType from '../messageContentType';\n\nimport GroupNotificationContent from './groupNotification';\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class CreateGroupNotification extends GroupNotificationContent {\n    creator = '';\n    groupName = '';\n\n    constructor(creator, groupName) {\n        super(MessageContentType.CreateGroup_Notification);\n        this.creator = creator;\n        this.groupName = groupName;\n    }\n\n    formatNotification() {\n        if (this.fromSelf) {\n            return '您创建了群组 ' + this.groupName;\n        } else {\n            return  webSocketCli.getDisplayName(this.creator)+' 创建了群组 ' + this.groupName;\n        }\n    }\n\n    encode() {\n        let payload = super.encode();\n        let obj = {\n            g: this.groupId,\n            n: this.groupName,\n            o: this.creator,\n        };\n        payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n        return payload;\n    }\n\n    decode(payload) {\n        super.decode(payload);\n        let json = StringUtils.b64_to_utf8(payload.binaryContent)\n        let obj = JSON.parse(json);\n        this.groupId = obj.g;\n        this.creator = obj.o;\n        this.groupName = obj.n;\n    }\n}"
  },
  {
    "path": "src/websocket/message/notification/dismissGroupNotification.js",
    "content": "import MessageContentType from '../messageContentType';\n\nimport GroupNotificationContent from './groupNotification';\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class DismissGroupNotification extends GroupNotificationContent {\n    operator = '';\n\n    constructor(operator) {\n        super(MessageContentType.DismissGroup_Notification);\n        this.operator = operator;\n    }\n\n    formatNotification() {\n        if (this.fromSelf) {\n            return '您解散了群组';\n        } else {\n            return webSocketCli.getDisplayName(this.operator) + '解散了群组';\n        }\n    }\n\n    encode() {\n        let payload = super.encode();\n        let obj = {\n            g: this.groupId,\n            o: this.operator,\n        };\n        payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n        return payload;\n    }\n\n    decode(payload) {\n        super.decode(payload);\n        let json = StringUtils.b64_to_utf8(payload.binaryContent)\n        let obj = JSON.parse(json);\n        this.groupId = obj.g;\n        this.operator = obj.o;\n    }\n}"
  },
  {
    "path": "src/websocket/message/notification/groupNotification.js",
    "content": "import NotificationMessageContent from \"./notificationMessageContent\";\n\nexport default class GroupNotificationContent extends NotificationMessageContent {\n    groupId = '';\n}"
  },
  {
    "path": "src/websocket/message/notification/kickoffGroupMemberNotification.js",
    "content": "import MessageContentType from \"../messageContentType\";\n\nimport GroupNotificationContent from \"./groupNotification\";\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class KickoffGroupMemberNotification extends GroupNotificationContent {\n    operator = '';\n    kickedMembers = [];\n\n    constructor(operator, kickedMembers) {\n        super(MessageContentType.KickOffGroupMember_Notification);\n        this.operator = operator;\n        this.kickedMembers = kickedMembers;\n    }\n\n    formatNotification() {\n        let notifyStr;\n        if (this.fromSelf) {\n            notifyStr = '您把 ';\n        } else {\n            notifyStr = webSocketCli.getDisplayName(this.operator) + '把 ';\n        }\n\n        let kickedMembersStr = '';\n        this.kickedMembers.forEach(m => {\n            kickedMembersStr += ' ' + webSocketCli.getDisplayName(m);\n        });\n\n        return notifyStr + kickedMembersStr + ' 移除了群组';\n    }\n\n    encode() {\n        let payload = super.encode();\n        let obj = {\n            g: this.groupId,\n            ms: this.kickedMembers,\n            o: this.operateUser,\n        };\n        payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n        return payload;\n    }\n\n    decode(payload) {\n        super.decode(payload);\n        let json = StringUtils.b64_to_utf8(payload.binaryContent)\n        let obj = JSON.parse(json);\n        this.groupId = obj.g;\n        this.operator = obj.o;\n        this.kickedMembers = obj.ms;\n    }\n}"
  },
  {
    "path": "src/websocket/message/notification/notificationMessageContent.js",
    "content": "import MessageContent from \"../messageContent\";\nexport default class NotificationMessageContent extends MessageContent {\n    // message#protoMessageToMessage时设置\n    fromSelf = false;\n    constructor(type) {\n        super(type);\n    }\n\n    digest(message) {\n        var desc = '';\n        try {\n            desc = this.formatNotification(message);\n        } catch (error) {\n            console.log('disgest', error);\n        }\n        return desc;\n    };\n\n    formatNotification(message) {\n        return '..nofication..';\n    }\n}"
  },
  {
    "path": "src/websocket/message/notification/quitGroupNotification.js",
    "content": "import MessageContentType from '../messageContentType';\n\nimport GroupNotificationContent from './groupNotification';\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class QuitGroupNotification extends GroupNotificationContent {\n    operator = '';\n\n    constructor(operator) {\n        super(MessageContentType.QuitGroup_Notification);\n        this.operator = operator;\n    }\n\n    formatNotification() {\n        if (this.fromSelf) {\n            return '您退出了群组';\n        } else {\n            return webSocketCli.getDisplayName(this.operator) + '退出了群组';\n        }\n    }\n\n    encode() {\n        let payload = super.encode();\n        let obj = {\n            g: this.groupId,\n            o: this.operator,\n        };\n        payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj));\n        return payload;\n    }\n\n    decode(payload) {\n        super.decode(payload);\n        let json = StringUtils.b64_to_utf8(payload.binaryContent)\n        let obj = JSON.parse(json);\n        this.groupId = obj.g;\n        this.operator = obj.o;\n    }\n}"
  },
  {
    "path": "src/websocket/message/notification/recallMessageNotification.js",
    "content": "import NotificationMessageContent from './notificationMessageContent'\nimport MessageContentType from '../messageContentType';\nimport StringUtils from '../../utils/StringUtil'\nimport webSocketCli from '../../websocketcli'\n\nexport default class RecallMessageNotification extends NotificationMessageContent {\n    operatorId = '';\n    messageUid = '';\n\n    constructor(operatorId, messageUid) {\n        super(MessageContentType.RecallMessage_Notification);\n        this.operatorId = operatorId;\n        this.messageUid = messageUid;\n    }\n\n    formatNotification() {\n        if(this.fromSelf){\n            return \"您撤回了一条消息\";\n        }else {\n            return webSocketCli.getDisplayName(this.operatorId) + \"撤回了一条消息\";\n        }\n    }\n\n    encode() {\n        let payload = super.encode();\n        payload.content = this.operatorId;\n        payload.binaryContent = StringUtils.utf8_to_b64(this.messageUid.toString());\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        this.operatorId = payload.content;\n        this.messageUid = StringUtils.b64_to_utf8(payload.binaryContent);\n    }\n}"
  },
  {
    "path": "src/websocket/message/persistFlag.js",
    "content": "export default class PersistFlag {\n    static No_Persist = 0;\n    static Persist = 1;\n    static Persist_And_Count = 3;\n    static Transparent = 4;\n}"
  },
  {
    "path": "src/websocket/message/protomessage.js",
    "content": "import MessageStatus from \"./messageStatus\";\nimport ConversationType from \"../model/conversationType\";\nimport ProtoMessageContent from \"./protomessageContent\";\nimport LocalStore from \"../store/localstore\";\n\n/**\n * 聊天信息content，在发送的时候转换为json消息体发送\n */\nexport default class ProtoMessage {\n    conversationType;\n    target;\n    line;\n    from = '';\n    content = {}; \n    messageId = '0';\n    direction = 0;\n    status = 0;\n    messageUid = '0';\n    timestamp = 0;\n    tos = '';\n\n\n    static toProtoMessage(obj){\n        let currentUserId = LocalStore.getUserId();\n        let protoMessage = new ProtoMessage();\n        if(obj.from == currentUserId){\n            protoMessage.direction = 0;\n            protoMessage.status = MessageStatus.Sent;\n        } else {\n            protoMessage.direction = 1;\n            protoMessage.status = MessageStatus.Unread;\n        }\n        protoMessage.tos = obj.tos;\n        protoMessage.messageId = obj.messageId;\n        protoMessage.messageUid = obj.messageId;\n        protoMessage.timestamp = obj.timestamp;\n        protoMessage.conversationType = obj.conversationType;\n        protoMessage.target = obj.target;\n        protoMessage.from = obj.from;\n        if(protoMessage.conversationType == ConversationType.Single){\n            if(obj.from != currentUserId){\n               protoMessage.target = obj.from;\n               protoMessage.from = obj.from;\n            } else {\n                protoMessage.target = obj.target;\n                protoMessage.from = obj.from;\n            }\n        }\n        protoMessage.line = obj.line;\n        protoMessage.content = ProtoMessageContent.toProtoMessageContent(obj.content);\n        return protoMessage;\n    }\n\n    /***\n     * 转为即将发送的protomessage\n     */\n    static convertToProtoMessage(message){\n        var protoMessage = new ProtoMessage();\n        protoMessage.conversationType = message.conversation.type;\n        protoMessage.target = message.conversation.target;\n        protoMessage.line = message.conversation.line;\n        protoMessage.from = message.from;\n        protoMessage.tos = message.tos;\n        protoMessage.direction = message.direction;\n        protoMessage.status = message.status;\n        protoMessage.messageId = message.messageId;\n        protoMessage.messageUid = message.messageUid;\n        protoMessage.timestamp = message.timestamp;\n        console.log(\"protomessage content \"+message.content)\n        var payload = message.content.encode();\n        protoMessage.content = ProtoMessageContent.toProtoMessageContent(payload);\n        return protoMessage;\n    }\n}"
  },
  {
    "path": "src/websocket/message/protomessageContent.js",
    "content": "import MessageContentType from \"./messageContentType\";\n\nexport default class ProtoMessageContent{\n    type;\n    searchableContent;\n    pushContent;\n    content;\n    binaryContent; // base64 string, 图片时，不包含头部信息:data:image/png;base64,\n    localContent;\n    mediaType;\n    remoteMediaUrl;\n    localMediaPath;\n    mentionedType = 0;\n    mentionedTargets = [];\n\n    static toProtoMessageContent(content){\n        var protoMessageContent = new ProtoMessageContent();\n        protoMessageContent.type = content.type;\n        protoMessageContent.content = content.content;\n        protoMessageContent.searchableContent = content.searchableContent;\n        protoMessageContent.pushContent = content.pushContent;\n        protoMessageContent.binaryContent = content.binaryContent;\n        protoMessageContent.localContent = content.localContent;\n        protoMessageContent.mediaType = content.mediaType;\n        protoMessageContent.remoteMediaUrl = content.remoteMediaUrl;\n        protoMessageContent.localMediaPath = content.localMediaPath;\n        protoMessageContent.mentionedType = content.mentionedType;\n        protoMessageContent.mentionedTargets = content.mentionedTargets;\n        return protoMessageContent;\n    }\n\n    static typeToContent(messageContent){\n        var showText = \"您有新的消息\";\n        switch(messageContent.type){\n            case MessageContentType.Image:\n                showText = \"[图片]\";\n                break;\n            case MessageContentType.File:\n                showText = \"[文件]\";\n                break;\n            case MessageContentType.Video:\n                showText = \"[视频]\";\n                break;\n            case MessageContentType.VOIP_CONTENT_TYPE_START:\n                showText = \"[网络电话]\";\n                break;\n            case MessageContentType.Tip_Notification:\n                showText =  '[通知消息]';   \n        }\n        return showText;\n    }\n}"
  },
  {
    "path": "src/websocket/message/sendMessage.js",
    "content": "export default class SendMessage{\n    target;\n    messageContent;\n    tos;\n\n    constructor(target,messageContent,tos=''){\n        this.target = target;\n        this.messageContent = messageContent;\n        this.tos = tos;\n    }\n}"
  },
  {
    "path": "src/websocket/message/textMessageContent.js",
    "content": "import MessageContent from './messageContent'\nimport MessagePayload from './messagePayload';\nimport MessageContentType from './messageContentType';\n\nexport default class TextMessageContent extends MessageContent {\n    content;\n\n    constructor(content, mentionedType = 0, mentionedTargets = []) {\n        super(MessageContentType.Text, mentionedType, mentionedTargets);\n        this.content = content;\n    }\n\n    digest() {\n        return this.content;\n    }\n\n    encode() {\n        let payload = super.encode();\n        payload.searchableContent = this.content;\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        this.content = payload.searchableContent;\n    }\n\n\n}"
  },
  {
    "path": "src/websocket/message/unknownMessageContent.js",
    "content": "import MessageContent from \"./messageContent\";\nimport MessageContentType from \"./messageContentType\";\n\nexport default class UnknownMessageContent extends MessageContent {\n    originalPayload;\n\n    constructor(originalPayload) {\n        super(MessageContentType.Unknown);\n        this.originalPayload = originalPayload;\n    }\n\n    encode() {\n        return this.originalPayload;\n    }\n\n    decode(paylaod) {\n        this.originalPayload = paylaod;\n    }\n\n    digest() {\n        return '未知类型消息';\n    }\n}"
  },
  {
    "path": "src/websocket/message/unsupportMessageContent.js",
    "content": "import MessageContent from \"./messageContent\";\n\nexport default class UnsupportMessageContent extends MessageContent {\n\n    digest() {\n        return '尚不支持该类型消息, 请手机查看 : ' + this.type;\n    }\n}"
  },
  {
    "path": "src/websocket/message/videoMessageContent.js",
    "content": "import MediaMessageContent from './mediaMessageContent'\nimport MessageContentMediaType from './messageContentMediaType';\nimport MessageContentType from './messageContentType';\n\nexport default class VideoMessageContent extends MediaMessageContent {\n    // base64 encoded\n    thumbnail;\n    constructor(fileOrLocalPath, remotePath, thumbnail) {\n        super(MessageContentType.Video, MessageContentMediaType.Video, fileOrLocalPath, remotePath);\n        this.thumbnail = thumbnail;\n    }\n\n    digest() {\n        return '[视频]';\n    }\n\n    encode() {\n        let payload = super.encode();\n        payload.binaryContent = this.thumbnail;\n        payload.mediaType = MessageContentMediaType.Video;\n        return payload;\n    };\n\n    decode(payload) {\n        super.decode(payload);\n        this.thumbnail = payload.binaryContent;\n    }\n}"
  },
  {
    "path": "src/websocket/message/websocketprotomessage.js",
    "content": "\n/**\n * websocket json 主协议\n * \n * {\n\t\"signal\": \"connect\",\n\t\"sub_signal\": \"conect_ack\",\n\t\"message_id\": 0,\n\t\"content\": \"\"\n    }\n */\nexport class WebSocketProtoMessage {\n    signal;\n    subSignal;\n\n    constructor(){\n        console.log('constuctor WebSocketProtoMessage');\n    }\n\n    setMessageId(messageId){\n        this.messageId = messageId;\n    }\n\n    setSignal(signal){\n        this.signal = signal;\n    }\n\n    setSubSignal(subSignal){\n        this.subSignal = subSignal;\n    }\n\n    setContent(content){\n        this.content = content; \n    }\n\n    toJson(){\n        let message = {\n            signal : this.signal,\n            subSignal : this.subSignal == null ? 'NONE' : this.subSignal,\n            messageId : this.messageId == null ? 0 : this.messageId,\n            content : this.content\n        }\n\n        return JSON.stringify(message);\n    }\n}"
  },
  {
    "path": "src/websocket/model/conversation.js",
    "content": "import ConversationType from \"./conversationType\";\n\n/**\n * \n        \"conversation\":{\n            \"conversationType\": 0, \n            \"target\": \"UZUWUWuu\", \n            \"line\": 0, \n        }\n */\nexport default class Conversation {\n    type = ConversationType.Single;\n    conversationType = this.type; // 这行是为了做一个兼容处理\n    target = '';\n    line = 0;\n\n    constructor(type, target, line) {\n        this.type = type;\n        this.conversationType = type;\n        this.target = target;\n        this.line = line;\n    }\n\n    equal(conversation) {\n        return this.type === conversation.type\n            && this.target === conversation.target\n            && this.line === conversation.line;\n    }\n}"
  },
  {
    "path": "src/websocket/model/conversationInfo.js",
    "content": "export default class ConversationInfo{\n    conversation = {};\n    lastMessage = {};\n    timestamp = 0;\n    draft = '';\n    unreadCount = {};\n    isTop = false;\n    isSilent = false;\n\n}"
  },
  {
    "path": "src/websocket/model/conversationType.js",
    "content": "export default class ConversationType {\n    static Single = 0;\n    static Group = 1;\n    static ChatRoom = 2;\n    static Channel = 3;\n}"
  },
  {
    "path": "src/websocket/model/groupInfo.js",
    "content": "import GroupType from './groupType'\nexport default class GroupInfo {\n    target = '';\n    name = '';\n    portrait = '';\n    owner = '';\n    type = GroupType.Normal;\n    memberCount = 0;\n    extra = '';\n    updateDt = 0;\n\n    static convert2GroupInfo(jsonObj){\n       var groupInfo = new GroupInfo();\n       groupInfo.target = jsonObj.target;\n       groupInfo.name = jsonObj.name;\n       groupInfo.portrait = jsonObj.portrait;\n       groupInfo.owner = jsonObj.owner;\n       groupInfo.type = jsonObj.type;\n       groupInfo.memberCount = jsonObj.memberCount;\n       groupInfo.extra = jsonObj.extra;\n       groupInfo.updateDt = jsonObj.updateDt;\n       return groupInfo;\n    }\n}"
  },
  {
    "path": "src/websocket/model/groupMember.js",
    "content": "export default class GroupMember {\n    groupId = '';\n    memberId = '';\n    alias = '';\n    type = 0;\n    updateDt = 0;\n    diplayName = '';\n    avatarUrl = '';\n\n    static convert2GroupMember(jsonObj){\n        var groupMember = new GroupMember();\n        groupMember.memberId = jsonObj.memberId;\n        groupMember.alias = jsonObj.alias;\n        groupMember.type = jsonObj.type;\n        groupMember.updateDt = jsonObj.updateDt;\n        return groupMember;\n    }\n}"
  },
  {
    "path": "src/websocket/model/groupMemberType.js",
    "content": "export default class GroupMemberType {\n    static Normal = 0;\n    static Manager = 1;\n    static Owner = 2;\n}"
  },
  {
    "path": "src/websocket/model/groupType.js",
    "content": "export default class GroupType {\n    static Normal = 0;\n    static Free = 1;\n    static Restricted = 2;\n}"
  },
  {
    "path": "src/websocket/model/protoConversationInfo.js",
    "content": "import UnreadCount from \"./unReadCount\";\n\nexport default class ProtoConversationInfo{\n    conversationType;\n    target;\n    line;\n    lastMessage = {};\n    timestamp = 0;\n    draft = '';\n    unreadCount = new UnreadCount();\n    isTop = false;\n    isSilent = false;\n}"
  },
  {
    "path": "src/websocket/model/stateConversationInfo.js",
    "content": "export default class StateConversationInfo{\n    name;\n    img;\n    //ProtoConversationInfo\n    conversationInfo = {};\n}"
  },
  {
    "path": "src/websocket/model/stateSelectChatMessage.js",
    "content": "export default class StateSelectChateMessage{\n    name = '';\n    target;\n    protoMessages = [];\n}"
  },
  {
    "path": "src/websocket/model/unReadCount.js",
    "content": "export default class UnreadCount {\n    // 单聊未读数\n    unread = 0;\n    // 群聊@数\n    unreadMention = 0;\n    // 群聊@All数\n    unreadMentionAll = 0;\n}"
  },
  {
    "path": "src/websocket/model/userInfo.js",
    "content": "export default class UserInfo {\n    uid = '';\n    name = '';\n    displayName = '';\n    gender = 0;\n    portrait = '';\n    mobile = '';\n    email = '';\n    address = '';\n    social = '';\n    extra = '';\n    type = 0; //0 normal; 1 robot; 2 thing;\n    updateDt = 1550652404513;\n\n    static convert2UserInfo(jsonObj){\n        let userInfo = new UserInfo();\n        userInfo.uid = jsonObj.uid;\n        userInfo.name = jsonObj.name;\n        userInfo.displayName = jsonObj.displayName;\n        userInfo.gender = jsonObj.gender;\n        userInfo.portrait = jsonObj.portrait;\n        userInfo.mobile = jsonObj.mobile;\n        userInfo.email = jsonObj.email;\n        userInfo.address = jsonObj.address;\n        userInfo.social = jsonObj.social;\n        userInfo.extra = jsonObj.extra;\n        userInfo.type = jsonObj.type;\n        userInfo.updateDt = jsonObj.updateDt;\n        return userInfo;\n    }\n}"
  },
  {
    "path": "src/websocket/store/localstore.js",
    "content": "import { KEY_VUE_USER_ID } from \"../../constant\";\n\n/**\n * 本地缓存，包括如下基本信息\n * 消息缓存\n * 消息当前序列号，用于消息拉取\n * 朋友列表信息\n * 群组信息\n */\nexport default class LocalStore {\n\n    static saveConverSations(value){\n        localStorage.setItem(\"coversations\",JSON.stringify(value));\n    }\n\n    static getConversations(){\n        let vaule = localStorage.getItem(\"coversations\");\n        return JSON.parse(vaule);\n    }\n\n    static getLastMessageSeq(){\n        return localStorage.getItem(\"last_message_seq\");\n    }\n\n    //设置上传token，根据不同media类型存储\n    static setUploadToken(key,token){\n        localStorage.setItem(key,token);\n    }\n\n    static getImageUploadToken(){\n        return localStorage.getItem(\"http://image.comsince.cn/\");\n    }\n\n    /**\n     * messageSeq为string类型\n     */\n    static setLastMessageSeq(messageSeq){\n        localStorage.setItem(\"last_message_seq\",messageSeq);\n    }\n\n    static saveMessages(value){\n        localStorage.setItem(\"messages\",JSON.stringify(value));\n    }\n\n    static getMessages(){\n        let value = localStorage.getItem(\"messages\");\n        return JSON.parse(value);\n    }\n\n    static saveUserInfoList(value){\n        localStorage.setItem(\"user_infos_list\",JSON.stringify(value));\n    }\n\n    static getUserInfoList(){\n        let value = localStorage.getItem(\"user_infos_list\");\n        return JSON.parse(value);\n    }\n\n    /**\n     * 记录消息发送条数主要时为了\n     */\n    static updateSendMessageCount(){\n        let value = localStorage.getItem(\"send_message_count\");\n        if(value){\n            value  = parseInt(value) + 1;\n        } else {\n            value = 1;\n        }\n        localStorage.setItem(\"send_message_count\",value);\n    }\n\n    static getSendMessageCount(){\n        let value = localStorage.getItem(\"send_message_count\");\n        if(!value){\n            value = 0;\n        }\n        return value;\n    }\n\n    static resetSendMessageCount(){\n        localStorage.setItem(\"send_message_count\",0);\n    }\n\n    static getUserId(){\n        return localStorage.getItem(KEY_VUE_USER_ID);\n    }\n\n    static setSelectTarget(value){\n        localStorage.setItem(\"select_target\",value);\n    }\n\n    static getSelectTarget(){\n        return localStorage.getItem(\"select_target\");\n    }\n\n    static setFriendRequestVersion(version){\n        localStorage.setItem(\"friend_request_version\",version);\n    }\n\n    static getFriendRequestVersion(){\n        var version = localStorage.getItem(\"friend_request_version\");\n        if(!version){\n            version = 0\n        }\n        return version;\n    }\n\n    static saveMessageId(messageId){\n       localStorage.setItem(\"message_id\",messageId);\n    }\n\n    static getMessageId(){\n        var messageId = localStorage.getItem(\"message_id\");\n        if(!messageId){\n            messageId = 0;\n        }\n        return messageId;\n    }\n\n    static clearLocalStore(){\n        localStorage.setItem(\"coversations\",\"\");\n        localStorage.setItem(\"last_message_seq\",\"\");\n        localStorage.setItem(\"messages\",\"\");\n        localStorage.setItem(\"send_message_count\",0);\n        localStorage.setItem(\"select_target\",\"\");\n        localStorage.setItem(\"friend_request_version\",0);\n        localStorage.setItem(KEY_VUE_USER_ID,'');\n        localStorage.setItem(\"message_id\",0);\n    }\n}"
  },
  {
    "path": "src/websocket/utils/StringUtil.js",
    "content": "export default class StringUtils {\n    static b64_to_utf8(str) {\n        return decodeURIComponent(escape(atob(str)));\n    }\n\n\n    static utf8_to_b64(str) {\n        return btoa(unescape(encodeURIComponent(str)));\n    }\n}"
  },
  {
    "path": "src/websocket/utils/aes.js",
    "content": "\nimport CryptoJS from 'crypto-js'\nconst iv = CryptoJS.enc.Base64.parse('ABEiM0RVZnd4eXp7fH1+fw==');\n\nexport function decrypt (text) {\n    let decrypted = CryptoJS.AES.decrypt(text, iv, {\n      iv: iv,\n      mode: CryptoJS.mode.CBC,\n      padding: CryptoJS.pad.Pkcs7\n    })\n    //由于服务端加密的时候前四个字节时间参数，现在暂时忽略前4字节\n    var resultWordArr = CryptoJS.enc.Hex.parse(decrypted.toString().slice(8));\n    let result = resultWordArr.toString(CryptoJS.enc.Utf8);\n    // let rmtimeResult = result.slice(4);\n    return result;\n  }\n\n\n  export function encrypt(encryptCode,key) {\n      console.log('code '+encryptCode+' key '+key);\n    let keyArry = new Array(16);\n    for(let i = 0; i<16; i++){\n        //这里是至打印字符编码\n        keyArry[i] = key.charCodeAt(i) & 0xFF;\n    }\n    let keyCode = String.fromCharCode.apply(String, keyArry);\n    let secretCode = CryptoJS.enc.Utf8.parse(keyCode);\n    console.log('keycode '+keyCode);\n\n    let timeEncryptCode = convertTimeEncryptCode(encryptCode);\n    console.log('timeEncryptCode '+timeEncryptCode);\n    \n    //pwd 必须base64解码\n\n    let encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Base64.parse(timeEncryptCode), secretCode, {\n         iv: secretCode, \n         mode: CryptoJS.mode.CBC, \n         padding: CryptoJS.pad.Pkcs7 \n        });\n    return encrypted.ciphertext.toString(CryptoJS.enc.Base64);\n}\n\nfunction convertTimeEncryptCode(encryptCode){\n    \n    // let encryptArr = string2Bin(encryptCode);\n   let encryptArr = CryptoJS.enc.Base64.parse(encryptCode);\n\n   encryptArr = wordToByteArray(encryptArr.words);\n    // encryptArr  = string2Bin(encryptArr);\n    let result = [];\n    let curhour = (new Date().getMilliseconds()/1000 - 1514736000)/3600;\n    for(var i=0; i < encryptArr.length + 4; i++){\n        if(i < 4){\n            if(i == 0){\n              result.push(curhour & 0xFF);\n            } else if(i == 1){\n              result.push((curhour & 0xFF00) >> 8);\n            } else if(i == 2){\n              result.push((curhour & 0xFF0000) >> 16);\n            } else if(i == 3){\n               result.push((curhour & 0xFF) >> 24);\n            }\n        } else{\n            result.push(encryptArr[i - 4]);\n        }\n    }\n    console.log('convertTimeEncryptCode result '+result);\n\n    // https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string\n    var base64wordarr = btoa(String.fromCharCode(...new Uint8Array(result)));\n    console.log(\"hash word \"+base64wordarr);\n\n    return base64wordarr;\n}\n\nfunction bin2String(array) {\n    var result = \"\";\n  for (var i = 0; i < array.length; i++) {\n    result += String.fromCharCode(parseInt(array[i], 2));\n  }\n  return result;\n}\n\nfunction string2Bin(str) {\n    var result = [];\n    for (var i = 0; i < str.length; i++) {\n      result.push(str.charCodeAt(i).toString(2));\n    }\n    return result;\n  }\n\n  function wordToByteArray(wordArray) {\n    var byteArray = [], word, i, j;\n    for (i = 0; i < wordArray.length; ++i) {\n        word = wordArray[i];\n        for (j = 3; j >= 0; --j) {\n            byteArray.push((word >> 8 * j) & 0xFF);\n        }\n    }\n    return byteArray;\n}\n\nfunction byteArrayToString(byteArray) {\n    var str = \"\", i;\n    for (i = 0; i < byteArray.length; ++i) {\n        str += escape(String.fromCharCode(byteArray[i]));\n    }\n    return str;\n}\n"
  },
  {
    "path": "src/websocket/utils/logger.js",
    "content": "export default class Logger {\n    static log(text){\n      var time = new Date();\n      console.log(\"[\" + time.toLocaleTimeString() + \"] \" + text);\n    }\n}"
  },
  {
    "path": "src/websocket/utils/timeUtils.js",
    "content": "export default class TimeUtils {\n    //参考链接: https://juejin.im/entry/5c7103f5f265da2dab17d1a6\n    static _formatDate(date, fmt){\n        var o = {\n            \"M+\": date.getMonth() + 1, //月份\n            \"d+\": date.getDate(), //日\n            \"h+\": date.getHours(), //小时\n            \"m+\": date.getMinutes(), //分\n            \"s+\": date.getSeconds(), //秒\n            \"q+\": Math.floor((date.getMonth() + 3) / 3), //季度\n            \"S\": date.getMilliseconds() //毫秒\n    };\n    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + \"\").substr(4 - RegExp.$1.length));\n    for (var k in o)\n            if (new RegExp(\"(\" + k + \")\").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : ((\"00\" + o[k]).substr((\"\" + o[k]).length)));\n                    return fmt;\n    }\n\n    static getTimeStringAutoShort2(timestamp,mustIncludeTime){\n         \n        // 当前时间\n        var currentDate = new Date();\n        // 目标判断时间\n        var srcDate = new Date(parseInt(timestamp));\n \n        var currentYear = currentDate.getFullYear();\n        var currentMonth = (currentDate.getMonth()+1);\n        var currentDateD = currentDate.getDate();\n \n        var srcYear = srcDate.getFullYear();\n        var srcMonth = (srcDate.getMonth()+1);\n        var srcDateD = srcDate.getDate();\n \n        var ret = \"\";\n \n        // 要额外显示的时间分钟\n        var timeExtraStr = (mustIncludeTime?\" \"+this._formatDate(srcDate, \"hh:mm\"):\"\");\n \n        // 当年\n        if(currentYear == srcYear) {\n                var currentTimestamp = currentDate.getTime();\n                var srcTimestamp = timestamp;\n                // 相差时间（单位：毫秒）\n                var deltaTime = (currentTimestamp-srcTimestamp);\n \n                // 当天（月份和日期一致才是）\n                if(currentMonth == srcMonth && currentDateD == srcDateD) {\n                // 时间相差60秒以内\n                if(deltaTime < 60 * 1000)\n                        ret = \"刚刚\";\n                // 否则当天其它时间段的，直接显示“时:分”的形式\n                else\n                        ret = this._formatDate(srcDate, \"hh:mm\");\n                }\n                // 当年 && 当天之外的时间（即昨天及以前的时间）\n                else {\n                        // 昨天（以“现在”的时候为基准-1天）\n                        var yesterdayDate = new Date();\n                        yesterdayDate.setDate(yesterdayDate.getDate()-1);\n \n                        // 前天（以“现在”的时候为基准-2天）\n                        var beforeYesterdayDate = new Date();\n                        beforeYesterdayDate.setDate(beforeYesterdayDate.getDate()-2);\n \n                        // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较，是最为准确的（如果用时间戳差值\n                        // 的形式，是不准确的，比如：现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00，\n                        // 这两者间只相差2小时，直接用“deltaTime/(3600 * 1000)” > 24小时来判断是否昨天，就完全是扯蛋的逻辑了）\n                        if(srcMonth == (yesterdayDate.getMonth()+1) && srcDateD == yesterdayDate.getDate())\n                                ret = \"昨天\"+timeExtraStr;// -1d\n                        // “前天”判断逻辑同上\n                        else if(srcMonth == (beforeYesterdayDate.getMonth()+1) && srcDateD == beforeYesterdayDate.getDate())\n                                ret = \"前天\"+timeExtraStr;// -2d\n                        else{\n                                // 跟当前时间相差的小时数\n                                var deltaHour = (deltaTime/(3600 * 1000));\n \n                                // 如果小于或等 7*24小时就显示星期几\n                                if (deltaHour <= 7*24){\n                                var weekday=new Array(7);\n                                weekday[0]=\"星期日\";\n                                weekday[1]=\"星期一\";\n                                weekday[2]=\"星期二\";\n                                weekday[3]=\"星期三\";\n                                weekday[4]=\"星期四\";\n                                weekday[5]=\"星期五\";\n                                weekday[6]=\"星期六\";\n \n                                // 取出当前是星期几\n                                var weedayDesc = weekday[srcDate.getDay()];\n                                ret = weedayDesc+timeExtraStr;\n                                }\n                                // 否则直接显示完整日期时间\n                                else\n                                ret = this._formatDate(srcDate, \"yy/M/d\")+timeExtraStr;\n                        }\n                }\n        }\n        // 往年\n        else{\n                ret = this._formatDate(srcDate, \"yy/M/d\")+timeExtraStr;\n        }\n \n        return ret;\n    }\n}"
  },
  {
    "path": "src/websocket/websocketcli.js",
    "content": "import Logger from \"./utils/logger\";\nimport vuexStore from '../store'\n\n\nexport class WebSocketClient {\n    \n    getDisplayName(userId){\n       var userInfolist = vuexStore.state.userInfoList;\n       var userInfo = userInfolist.find(user => user.uid == userId);\n       var displayName = userId;\n       var friendData = vuexStore.state.friendDatas.find(friend => friend.friendUid == userId)\n       if(friendData && friendData.alias && friendData.alias != \"\"){\n           displayName = friendData.alias\n       } else if(userInfo){\n          displayName = userInfo.displayName;\n          if(displayName == ''){\n             displayName = userInfo.mobile;\n          }\n       } else {\n         vuexStore.state.vueSocket.getUserInfo(userId);\n       }\n      //    console.log(\"userId \"+userId +\" displayName \"+displayName) \n       return displayName;\n    }\n\n    getPortrait(userId){\n      var userInfolist = vuexStore.state.userInfoList;\n      var userInfo = userInfolist.find(user => user.uid == userId);\n      var portrait = 'static/images/vue.jpg'\n      if(userInfo){\n          portrait = userInfo.portrait;\n      }\n      return portrait;\n    }\n\n\n    createGroup(groupName,memberIds){\n       return vuexStore.state.vueSocket.createGroup(groupName,memberIds);\n    }\n\n    modifyGroupInfo(info){\n       return vuexStore.state.vueSocket.modifyGroupInfo(info);\n    }\n\n    quitGroup(groupId){\n       return vuexStore.state.vueSocket.quitGroup(groupId);\n    }\n\n    dismissGroup(groupId){\n       return vuexStore.state.vueSocket.dismissGroup(groupId);\n    }\n\n    getGroupMember(groupId){\n       return vuexStore.state.vueSocket.getGroupMember(groupId);\n    }\n\n    addMembers(groupId, memberIds){\n       return vuexStore.state.vueSocket.addMembers(groupId,memberIds);\n    }\n\n    kickeMembers(groupId,memberIds){\n       return vuexStore.state.vueSocket.kickeMembers(groupId,memberIds);\n    }\n\n    recallMessage(messageUid){\n       return vuexStore.state.vueSocket.recallMessage(messageUid);\n    }\n\n    getMinioUploadUrl(mediaType,key){\n       return vuexStore.state.vueSocket.getMinioUploadUrl(mediaType,key);\n    }\n  \n    modifyFriendAlias(targetUid,alias){\n       return vuexStore.state.vueSocket.modifyFriendAlias(targetUid,alias)\n    }\n\n    //注意beforeUid最好为string类型\n    getRemoteMessages(conversation,beforeUid,count){\n      return vuexStore.state.vueSocket.getRemoteMessages(conversation,beforeUid,count)\n    }\n\n}\n\nconst self = new WebSocketClient();\nexport default self;"
  },
  {
    "path": "static/.gitkeep",
    "content": ""
  },
  {
    "path": "static/css/reset.css",
    "content": "html, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video, input {\n    margin: 0;\n    padding: 0;\n    border: 0;\n    font-size: 100%;\n    font-weight: normal;\n    vertical-align: baseline;\n}\n\narticle, aside, details, figcaption, figure,\nfooter, header, menu, nav, section {\n    display: block;\n}\n\nbody {\n    line-height: 1;\n    overflow: hidden;\n}\n\nblockquote, q {\n    quotes: none;\n}\n\nblockquote:before, blockquote:after,\nq:before, q:after {\n    content: none;\n}\n\ntable {\n    border-collapse: collapse;\n    border-spacing: 0;\n}\n\na {\n    color: #7e8c8d;\n    text-decoration: none;\n    -webkit-backface-visibility: hidden;\n}\n\nli {\n    list-style: none;\n}\n\n\nhtml, body {\n    width: 100%;\n    height: 100%;\n}\nbody {\n    -webkit-text-size-adjust: none;\n    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n    background: #466368;\n    background: -webkit-linear-gradient(#648880, #293f50);\n    background:    -moz-linear-gradient(#648880, #293f50);\n    background:         linear-gradient(#648880, #293f50);\n    background-size: cover;\n}\n\n/*在mac上，增加此设置，会导致滚动条不能自动显示与隐藏，滚动条会一直显示 */\n::-webkit-scrollbar {\n    width: 8px;\n}\n\n/* 滚动条滑块 */\n::-webkit-scrollbar-thumb {\n    border-radius: 6px;\n    background: rgba(0,0,0,0.1);\n}\n\n/* video {\n    background: #222;\n    margin: 0 0 20px 0;\n    --width: 100%;\n    width: var(--width);\n    height: var(--width);\n} */\n\n\n*,*:before,*:after {\n\t-moz-box-sizing: border-box;\n\t-webkit-box-sizing: border-box;\n\tbox-sizing: border-box\n}\n\n.flexbox{display:-webkit-box; display:-webkit-flex; display:flex; display:-ms-flexbox;}\n.flex-alignc{align-items: center;}\n.flex1{-webkit-box-flex:1; -webkit-flex:1; -ms-flex:1; flex:1;}"
  }
]