在《开源企业即时通讯和在线客服》中已介绍了Lesktop的桌面模式和Web模式,但是没有移动端。评论中 dotnetcms.org工作室 提到了LayIM,看了一下官网的演示和文档,如果用这套LayIM的移动端Lestop也可以轻松开发出移动端web版本。本文将说明如何接入LayIM移动端UI,同时对一些Lesktop的接口进行说明,作为接入其他前端UI的指引。

移动端功能展示:

 

源代码下载:https://files.cnblogs.com/files/lucc/IM3.1.zip

源代码Git: https://github.com/luchuncheng/Lesktop.git

在线演示

移动端:http://im.luchuncheng.com/mobile.aspx

注册用户&内部人员

Web版:http://im.luchuncheng.com 

PC版下载:http://client.luchuncheng.com

客服平台(访客端)

Web版:http://service.luchuncheng.com

PC版http://im.luchuncheng.com/client.ashx?chatwith=4&embedcode=1&createaccount=true 
       (embedcode=1表示显示ID=1的客服嵌入代码指定的客服人员,chatwith=4表示启动和ID=4的客服人员对话窗口)

1、登录

接入的第一个步骤就是登录,登录界面非常简单,就是两个文本框和一个登录按钮,服务单只需要调用ServerImpl.Instance.Login即可:

int userid = AccountImpl.Instance.GetUserID(user);
// 仅验证不启动回话,重定向到default.aspx再启动回话
ServerImpl.Instance.Login("", Context, userid, false, null, false, 2);

最后第二个参数startSession=false,表示只是设置cookie不启动会话,移动端的login.aspx仅仅只是验证,登录后重定向到default.aspx再启动会话

最后一个参数device=2表示登录设备为移动端web版

2、初始化

LayIM初始化时需要好友,群组和分组等信息,因此登录完成后需要提供这些数据。在此之前先了解一下Lesktop的常用联系人功能:


如上图所示,Lesktop允许用户自己创建常用联系人分组,支持无限层级,用户可以将好友或内部人员添加到自建的任何层级的分组中。由于LayIM不支持多层次分组,所以在移动端中将所有常用联系人不分层级显示,如下图所示

除了常用联系人,还需要一个“我的好友”分组,用于显示已加自己为好友的注册用户。接下来需要了解几个和分组,好友和群组相关的接口:

(1) ServerImpl.Instance.CommonStorageImpl.GetCategories

GetCategories用于获取用户创建的所有分组,返回值是一个DataRowCollection,每一行包括5个列:

ID   类别ID
UserID  添加到该列表的联系人ID
Name  分组名称
ParentID  父级分组ID(移动端用不上)
Type  分组类别(1:联系人,2:群组,3:部门,移动端只用到联系人的)

(2) ServerImpl.Instance.CommonStorageImpl.GetCategoryItems

GetCategoryItems获取和分组相关的所有联系人,群组和部门ID(移动端只用到联系人),返回值为DataRowCollection,每一行包括4个列:

UserID 创建该分类的用户ID
CategoryID 类别ID
ItemID 联系人,群组或部门ID(移动端只用到联系人)
CategoryType 分组类别(1:联系人,2:群组,3:部门,移动端只用到联系人的)

(3) Category_CH.GetAccountInfos

GetCategoryItems只能获取和分组相关的所有联系人的ID,还需要调用GetAccountInfos才能获取到联系人的全部详细信息,返回值为AccountInfo数组,AccountInfo属性如下:

ID 用户ID
Name 登录名
Nickname 昵称
Type 类型,0-联系人,1-群组
SubType 子类型:0-注册用户,1-管理员创建的内部人员
IsTemp 是否为临时用户,即访客
IsDeleted 是否已被删除
HeadIMG 头像

(4) AccountImpl.Instance.GetVisibleUsersDetails

GetVisibleUsersDetails用于获取所有和指定用户相关的联系人和群组,包括所有由管理员创建的内部人员,已加自己为好友的注册用户,自己创建和加入的所有群组和自己创建或被拉进去的多人会话,这部分数据主要是为LayIM提供“我的好友”分组和群聊,返回值为一个Hashtable,每个项的值为AccountInfo。

(5)ServerImpl.Instance.GetCurrentUser

GetCurrentUser用户获取当前用户详细信息(AccountInfo)

以上5个接口已经获取到了所有LayIM初始化需要的数据,打包成json,“赋值”给页面的MobileInitParams全局变量即可:

namespace Core.Web
{
public class Mobile_Default : System.Web.UI.Page
{
string init_params_ = "{}"; protected void Page_Load(object sender, EventArgs e)
{
AccountInfo current_user = ServerImpl.Instance.GetCurrentUser(Context);
if(current_user != null)
{
String sessionId = Guid.NewGuid().ToString().ToUpper();
ServerImpl.Instance.Login(sessionId, Context, current_user.ID, false, DateTime.Now.AddDays(7), true, 2); DataRowCollection categories = ServerImpl.Instance.CommonStorageImpl.GetCategories(current_user.ID); DataRowCollection items = ServerImpl.Instance.CommonStorageImpl.GetCategoryItems(current_user.ID);
Hashtable users = Category_CH.GetAccountInfos(items); AccountInfo[] visible_users = AccountImpl.Instance.GetVisibleUsersDetails(current_user.Name); init_params_ = Utility.RenderHashJson(
"Result", true,
"IsLogin", true,
"UserInfo", current_user.DetailsJson,
"SessionID", sessionId,
"CompanyInfo", ServerImpl.Instance.CommonStorageImpl.GetCompanyInfo(),
"Categories", categories,
"CategorieItems", items,
"CategorieUsers", users,
"VisibleUsers", visible_users
);
}
else
{
Response.Redirect("login.aspx");
}
} public string InitParams
{
get { return init_params_; }
}
}
}

  

页面加载后,LayIM_Init里面就可以通过MobileInitParams获取到这些数据,LayIM初始化参数请看官网文档,以下函数用于将Lesktop的数据转换成LayIM需要的格式:

// 获取分组和联系人
function GetFriends()
{
var friends = [];
for (var i = 0; i < window.MobileInitParams.Categories.length; i++)
{
var category = window.MobileInitParams.Categories[i];
if (category.Type == 1)
{
// Type=1为常用联系人类别,将所有常用联系人类别(不分层次)显示为LayIM的分组
var groupid = category.ID + 10000;
var group = {
"groupname": category.Name,
"id": groupid.toString(),
"online": 0,
"list": []
};
var user_count = 0;
var online_count = 0;
for (var j = 0; j < window.MobileInitParams.CategorieItems.length; j++)
{
// 从CategorieItems中获取该分组所有联系人ID
var item = window.MobileInitParams.CategorieItems[j];
if (item.CategoryID == category.ID)
{
// 通过联系人ID从CategorieUsers中获取联系人详细信息
var friend_info = window.MobileInitParams.CategorieUsers[item.ItemID.toString()];
if(friend_info != undefined)
{
group.list.push({
"username": friend_info.Nickname,
"id": friend_info.ID.toString(),
"avatar": Core.CreateHeadImgUrl(friend_info.ID, 150, false, friend_info.HeadIMG),
"sign": ""
});
user_count++;
if (friend_info.State == "Online")
{
online_count++;
}
}
}
}
if (user_count > 0)
{
friends.push(group);
}
}
} var grou_myfriend = {
"groupname": "我的好友",
"id": LayIMGroup_MyFriend,
"online": 0,
"list": []
} var current_user = window.MobileInitParams.UserInfo; // 获取所有好友并显示到好友分组
for (var i = 0; i < window.MobileInitParams.VisibleUsers.length; i++)
{
var user = window.MobileInitParams.VisibleUsers[i];
if (user.Type == 0 && ((current_user.SubType == 1 && user.SubType == 0) || current_user.SubType == 0))
{
// 内部人员(SubType=1)显示注册用户并添加自己为好友的,不包括其他内部人员
// 注册用户(SubType=0)显示添加自己为好友的其他注册用户和内部用户
grou_myfriend.list.push({
"username": user.Nickname,
"id": user.ID.toString(),
"avatar": Core.CreateHeadImgUrl(user.ID, 150, false, user.HeadIMG),
"sign": ""
});
}
} friends.push(grou_myfriend); friends.push({
"groupname": "其他联系人",
"id": LayIMGroup_Other,
"online": 0,
"list": []
}); return friends;
}
// 获取群聊
function GetGroups()
{
// 获取所有群组和多人会话
var groups = [];
for (var i = 0; i < window.MobileInitParams.VisibleUsers.length; i++)
{
var user = window.MobileInitParams.VisibleUsers[i];
if(user.Type == 1)
{
groups.push({
"groupname": user.Nickname,
"id": user.ID.toString(),
"avatar": Core.CreateGroupImgUrl(user.HeadIMG, user.IsTemp)
});
}
}
return groups;
}  

3、接收消息

此次为了接入LayIM,加了一个全局委托Core.OnNewMessage,每当收到新消息是会调用该委托,如果需要监听新消息,只需要附加一个处理函数即可

function LayIM_OnNewMessage(msg)
{
}
// 监听新消息
Core.OnNewMessage.Attach(LayIM_OnNewMessage);

由于收到的消息可能是web或pc端发送的,包含LayIM消息面板不支持的富文本,因此需要先处理掉所有HTML tag,此外还需要处理文件标志([FILE:...])生成下载链接,完整代码如下:

function LayIM_ParseMsg(text)
{
var newText = text;
try
{
// 处理掉HTML开始TAG
newText = text.toString().replace(
/<([a-zA-Z0-9]+)([\s]+)[^<>]*>/ig,
function (html, tag)
{
if (tag.toLowerCase() == "img")
{
var filename = Core.GetFileNameFromImgTag(html);
if (filename != "")
{
// Lesktop服务器上的文件,重新加上分辨率限制参数,改为下载缩略图,链接到原图
var url = Core.CreateDownloadUrl(filename);
return String.format("a({0})[img[{0}&MaxWidth=450&MaxHeight=800]]", url);
}
else
{
// 外源图片,改成超链接,防止下载图片浪费流量
var src = Core.GetSrcFromImgTag(html);
return String.format("a({0})[{1}]", src, "&nbsp;图片&nbsp;");
}
}
return "";
}
)
.replace(
/\x5BFILE:([^\x5B\x5D]+)\x5D/ig,
function (filetag, filepath)
{
// 提取文件消息,改为视频,音频或文件
var path = unescape(filepath)
var ext = Core.Path.GetFileExtension(path).toLowerCase();
if (ext == ".mp4" || ext == ".mov")
{
return String.format("video[{0}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path));
}
else if (ext == "mp3")
{
return String.format("audio[{0}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path));
}
else
{
return String.format("file({0})[{1}]", Core.CreateDownloadUrl(path), Core.Path.GetFileName(path));
}
}
)
.replace(
/<([a-zA-Z0-9]+)[\x2F]{0,1}>/ig,
function (html, tag)
{
// 清理<br/>等
return "";
}
)
.replace(
/<\/([a-zA-Z0-9]+)>/ig,
function (html, tag)
{
// 清理HTML结束TAG
return "";
}
);
}
catch(ex)
{
newText += " ERROR:";
newText += ex.message;
}
return newText;
} function LayIM_OnNewMessage(msg)
{
// msg.Sender, msg.Receiver只包括最基本的ID,Name,需重新获取详细信息
var sender_info = Core.AccountData.GetAccountInfo(msg.Sender.ID);
if (sender_info == null) sender_info = msg.Sender;
var receiver_info = Core.AccountData.GetAccountInfo(msg.Receiver.ID);
if (receiver_info == null) receiver_info = msg.Receiver; if (msg.Receiver.Type == 0)
{
// 私聊消息
if (!LayIM_UserExists(sender_info.ID.toString()))
{
// 分组列表中不包括消息发送者,将发送者加入到其他联系人分组
layim.addList({
type: 'friend',
"username": sender_info.Nickname,
"id": sender_info.ID.toString(),
"groupid": LayIMGroup_Other,
"avatar": Core.CreateHeadImgUrl(sender_info.ID, 150, false, sender_info.HeadIMG),
"sign": ""
});
}
// 显示到LayIM消息面板
layim.getMessage({
username: sender_info.Nickname,
avatar: Core.CreateHeadImgUrl(msg.Sender.ID, 150, false, sender_info.HeadIMG),
id: msg.Sender.ID.toString(),
type: "friend",
cid: msg.ID.toString(),
content: LayIM_ParseMsg(msg.Content)
});
}
else if (msg.Receiver.Type == 1)
{
// 群消息
if (!LayIM_GroupExists(receiver_info.ID.toString()))
{
// 群聊列表中不包括该群,加入到群聊中
layim.addList({
"type": "group",
"groupname": receiver_info.Nickname,
"id": receiver_info.ID.toString(),
"avatar": Core.CreateGroupImgUrl(receiver_info.HeadIMG, receiver_info.IsTemp)
});
}
// 显示到LayIM消息面板
layim.getMessage({
username: sender_info.Nickname,
avatar: Core.CreateHeadImgUrl(msg.Sender.ID, 150, false, sender_info.HeadIMG),
id: msg.Receiver.ID.toString(),
type: "group",
cid: msg.ID.toString(),
content: LayIM_ParseMsg(msg.Content)
});
}
} // 监听新消息
Core.OnNewMessage.Attach(LayIM_OnNewMessage);

4、发送消息

发送消息只需要调用服务端的WebIM.NewMessage方法即可,发送前,需要对消息进行预处理,把LayIM的标志(图片,文件和表情)转换成HTML,还需要调用Core.TranslateMessage,该函数用于将消息中的图片(<img ...>),文件([FILE:...])转换成服务端可以处理的附件,具体代码如下:

function LayIM_SendMsg_GetFileName(fileurl)
{
var filename_regex = /FileName\=([^\s\x28\x29\x26]+)/ig;
filename_regex.lastIndex = 0
var ret = filename_regex.exec(fileurl);
if (ret == null || ret.length <= 1)
{
return "";
}
return ret[1];
} function LayIM_SendMsg(data)
{
var msgdata = {
Action: "NewMessage",
Sender: parseInt(data.mine.id, 10),
Receiver: parseInt(data.to.id, 10),
DelTmpFile: 0,
Content: ""
}; var content = data.mine.content;
// 转换图片消息
content = content.replace(
/img\x5B([^\x5B\x5D]+)\x5D/ig,
function(imgtext, src)
{
var filename = LayIM_SendMsg_GetFileName(src);
return String.format('<img src="{0}">', Core.CreateDownloadUrl(filename));
}
);
// 转换文件消息
content = content.replace(
/file\x28([^\x28\x29]+)\x29\x5B([^\x5B\x5D]+)\x5D/ig,
function (filetext, fileurl, ope)
{
var path = unescape(LayIM_SendMsg_GetFileName(fileurl));
return Core.CreateFileHtml([path]);
}
);
// 将消息中的图片(<img ...>),文件([FILE:...])转换成服务端可以处理的附件
content = Core.TranslateMessage(content, msgdata);
// 转换表情
content = content.replace(
/face\[([^\s\[\]]+?)\]/g,
function (face, face_type)
{
var face_file = LayIM_FaceToFile[face_type];
if(face_file != undefined)
{
return String.format('<img src="{0}/{1}">', Core.GetUrl("layim/images/face"), face_file);
}
}
); msgdata.Content = content; Core.SendCommand(
function (ret)
{
var message = ret;
},
function (ex)
{
var errmsg = String.format("由于网络原因,消息\"{0}\"发送失败,错误信息:{1}", text, ex.Message);
},
Core.Utility.RenderJson(msgdata), "Core.Web WebIM", false
);
}

5、异常状态处理

Lesktop有以下几种异常状态:

(1) 在其他浏览器或客户端登录,此时会收到强制下线通知(GLOBAL:OFFLINE)

(2) 服务端已升级,为简化服务端开发,Lesktop服务端要求客户端和前端都用对应的最新版本,不兼容旧版本。服务端网站升级后,升级前未退出重新连接上的客户端和web端都会收到不兼容异常通知(IncompatibleException)。PC需要重启升级,WEB端需要重登陆(发布版所有静态资源都放在名称为版本号的文件夹中,重登陆不会读取到缓存的资源)

(3) 验证身份异常,服务端网站可能会因为某种原因重新启动,此时会重新生成cookie加密密钥,会导致已在线的客户端无法从cookie获取身份信息,此时客户端会收到验证异常通知(UnauthorizedException)

移动端处理异常方法很简单,收到异常通知后,立刻重定向到offline.aspx页面,显示异常消息和重新登录按钮,如下图所示:
  

至此,接入LayIM的工作就基本完成,前端代码基本都在mobile.js中。

因为LayIM不是开源的,因此git上不包括LayIM的源代码,需要自行购买,然后将src下的所有文件放到CurrentVersion/layim下:

最新文章

  1. .Net开发笔记(二十一) 反射在.net中的应用
  2. TortoiseSVN提交提示423 Locked的解决办法
  3. 108 vpn iptables
  4. el表达式无法获取springmvc的model封装好的数据之解决方法
  5. c语言中static的用法,包括全局变量和局部变量用static修饰
  6. PyCharm 3.0 发布,提供免费开源版本
  7. [Ubuntu] Install subversion1.8 on Ubuntu13.10
  8. K-Anonymous Sequence(poj 3709)
  9. Linux网络编程5&mdash;&mdash;使用UDP协议实现群聊
  10. OpenGL1-6讲小结
  11. &lt;script runat=server&gt;、&lt;%%&gt;和&lt;%#%&gt;的区别
  12. Java基础知识强化29:String类之String类构造方法
  13. 黑马程序员_Java_String
  14. Nutch2.3分布执行过程中Mongodb中数据的变化
  15. ARC注意的泄漏问题
  16. nvm进行node多版本管理
  17. Nginx-动态路由升级版
  18. 关于SELinux
  19. 随心测试_职场面试_001&lt;SX的面试观点&gt;
  20. Python request 在linux上持续并发发送HTTP请求遇到 Failed to establish a new connection: [Errno 11] Resource temporarily unavailable

热门文章

  1. 攻防世界——Misc新手练习区解题总结&lt;1&gt;(1-4题)
  2. Fastlane- app自动编译、打包多个版本、上传到app store
  3. Timeline Event
  4. openshift搭建私有registry
  5. leetcode刷题-83删除排序链表中的重复元素
  6. CSS的坑
  7. vue父子传值与非父子传值
  8. Popular Cows(POJ 2186)
  9. Redis源码笔记--服务器日志和函数可变参数处理server.c
  10. python中绝对值的表达式