如何使用firebase在ios中实现私人聊天

时间:2020-12-05 21:25:12

I have implemented private chat in my ios app. However, it is not so private. When I send a message that I intend to send to one person, everyone in the app can see it. I have three view controllers in play here. 如何使用firebase在ios中实现私人聊天

我在ios应用程序中实现了私聊。但是,它不是那么私密。当我发送一条我打算发送给一个人的消息时,应用程序中的每个人都可以看到它。我这里有三个视图控制器。

The FirstViewController has a list of users, and when the cell is clicked it is segued to the DetailedViewController. In this viewController, it only lists the details of the user clicked on. Next, when I press the compose button in the DetailedViewController, the goal is to segue to MessageUserController. This is where I am stuck. This is the code to segue to the MessageUserController:

FirstViewController有一个用户列表,当单击该单元格时,它将被调用到DetailedViewController。在此viewController中,它仅列出单击的用户的详细信息。接下来,当我按下DetailedViewController中的compose按钮时,目标是转到MessageUserController。这是我被困的地方。这是要转到MessageUserController的代码:

var username: String?

@IBAction func sendMessage(_ sender: Any) {
    performSegue(withIdentifier: "sendMessageToUser", sender: self.username)
}

override public func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard segue.identifier == "sendMessageToUser", let chatVc = segue.destination as? MessageViewController else {
        return
    } 
    chatVc.senderId = self.loggedInUser?.uid
    chatVc.senderDisplayName = self.username
}

I assume the sender could be the username because it is unique to the user. When I click on a user to chat with, it works fine but when I click on another user, the chat between the first users are already displayed in the new user's chatController

我假设发件人可能是用户名,因为它对用户来说是唯一的。当我点击用户聊天时,它工作正常但当我点击另一个用户时,第一个用户之间的聊天已经显示在新用户的chatController中

In the firstViewController, username is passed like this:

在firstViewController中,用户名的传递方式如下:

if segue.identifier == "UsersProfile" {
    if let indexPath = sender as? IndexPath{
        let vc = segue.destination as! UsersProfileViewController
        let post = self.posts[indexPath.row] as! [String: AnyObject]
        let username = post["username"] as? String
        vc.username = username 
    }
}

entire view controller:

整个视图控制器:

import UIKit
import Photos
import Firebase
import FirebaseDatabase
import JSQMessagesViewController

class SendMessageViewController: JSQMessagesViewController {
    var username: String?
    //var receiverData = AnyObject?()

    var messages = [JSQMessage]()
    private var photoMessageMap = [String: JSQPhotoMediaItem]()
    private let imageURLNotSetKey = "NOTSET"
    lazy var outgoingBubbleImageView: JSQMessagesBubbleImage = self.setupOutgoingBubble()
    lazy var incomingBubbleImageView: JSQMessagesBubbleImage = self.setupIncomingBubble()
    var rootRef = FIRDatabase.database().reference()
    var messageRef = FIRDatabase.database().reference().child("messages")
    private var newMessageRefHandle: FIRDatabaseHandle?
    private lazy var usersTypingQuery: FIRDatabaseQuery =
        self.rootRef.child("typingIndicator").queryOrderedByValue().queryEqual(toValue: true)
    lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "gs://gsignme-14416.appspot.com")
    private var updatedMessageRefHandle: FIRDatabaseHandle?
    private lazy var userIsTypingRef: FIRDatabaseReference =
        self.rootRef.child("typingIndicator").child(self.senderId) // 1
    private var localTyping = false // 2
    var isTyping: Bool {
        get {
            return localTyping
        }
        set {
            // 3
            localTyping = newValue
            userIsTypingRef.setValue(newValue)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.senderId = FIRAuth.auth()?.currentUser?.uid

        // Do any additional setup after loading the view.
        self.navigationController?.navigationBar.barTintColor = UIColor(red:0.23, green:0.73, blue:1.00, alpha:1.0)
        self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.white]

        self.navigationItem.title = senderDisplayName
        self.navigationItem.rightBarButtonItem?.tintColor = UIColor.white
        self.navigationItem.leftBarButtonItem?.tintColor = UIColor.white

        // No avatars
        collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
        collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero

         observeMessages()


    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        observeTyping()

    }

    deinit {
        if let refHandle = newMessageRefHandle {
            messageRef.removeObserver(withHandle: refHandle)

        }

        if let refHandle = updatedMessageRefHandle {
            messageRef.removeObserver(withHandle: refHandle)
        }

    }


    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
        return messages[indexPath.item]
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return messages.count
    }

    private func setupOutgoingBubble() -> JSQMessagesBubbleImage {
        let bubbleImageFactory = JSQMessagesBubbleImageFactory()
        return bubbleImageFactory!.outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
    }

    private func setupIncomingBubble() -> JSQMessagesBubbleImage {
        let bubbleImageFactory = JSQMessagesBubbleImageFactory()
        return bubbleImageFactory!.incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleLightGray())
    }

    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
        let message = messages[indexPath.item] // 1
        if message.senderId == senderId { // 2
            return outgoingBubbleImageView
        } else { // 3
            return incomingBubbleImageView
        }
    }

    override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
        return nil
    }

    override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat {
        return 15
    }


    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
        let message = messages[indexPath.item]

        if message.senderId == senderId {
            cell.textView?.textColor = UIColor.white
        } else {
            cell.textView?.textColor = UIColor.black
        }
        return cell
    }

    //ADD A NEW MESSAGE
    private func addMessage(withId id: String, name: String, text: String) {
        if let message = JSQMessage(senderId: id, displayName: name, text: text) {
            messages.append(message)
        }
    }

    override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {

        let itemRef = rootRef.child("messages").childByAutoId() // 1
        let messageItem = [ // 2
            "senderId": senderId!,
            "ReceiverName": senderDisplayName!,
            "text": text!,

            ]

        itemRef.setValue(messageItem) // 3

        JSQSystemSoundPlayer.jsq_playMessageSentSound() // 4

        finishSendingMessage() // 5
        isTyping = false
    }

    private func observeMessages() {
               // 1.
        let messageQuery = rootRef.child("messages").queryLimited(toLast: 25)


        // 2. We can use the observe method to listen for new
        // messages being written to the Firebase DB
        newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) -> Void in
            // 3
            let messageData = snapshot.value as! Dictionary<String, String>

            if let id = messageData["senderId"] as String!, let name = messageData["ReceiverName"] as String!, let text = messageData["text"] as String!, text.characters.count > 0 {
                // 4
                self.addMessage(withId: id, name: name, text: text)

                // 5
                self.finishReceivingMessage()
            } else if let id = messageData["senderId"] as String!,
                let photoURL = messageData["photoURL"] as String! { // 1
                // 2
                if let mediaItem = JSQPhotoMediaItem(maskAsOutgoing: id == self.senderId) {
                    // 3
                    self.addPhotoMessage(withId: id, key: snapshot.key, mediaItem: mediaItem)
                    // 4
                    if photoURL.hasPrefix("gs://") {
                        self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: nil)
                    }
                }
            } else {
                print("Error! Could not decode message data")
            }
        })
        // We can also use the observer method to listen for
        // changes to existing messages.
        // We use this to be notified when a photo has been stored
        // to the Firebase Storage, so we can update the message data
        updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
            let key = snapshot.key
            let messageData = snapshot.value as! Dictionary<String, String> // 1

            if let photoURL = messageData["photoURL"] as String! { // 2
                // The photo has been updated.
                if let mediaItem = self.photoMessageMap[key] { // 3
                    self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key) // 4
                }
            }
        })
    }

    override func textViewDidChange(_ textView: UITextView) {
        super.textViewDidChange(textView)
        // If the text is not empty, the user is typing
        isTyping = textView.text != ""
    }
    private func observeTyping() {
        let typingIndicatorRef = rootRef.child("typingIndicator")
        userIsTypingRef = typingIndicatorRef.child(senderId)
        userIsTypingRef.onDisconnectRemoveValue()
        usersTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqual(toValue: true)

        // 1
        usersTypingQuery.observe(.value) { (data: FIRDataSnapshot) in
            // 2 You're the only one typing, don't show the indicator
            if data.childrenCount == 1 && self.isTyping {
                return
            }

            // 3 Are there others typing?
            self.showTypingIndicator = data.childrenCount > 0
            self.scrollToBottom(animated: true)
        }
    }

    func sendPhotoMessage() -> String? {
        let itemRef = messageRef.childByAutoId()

        let messageItem = [
            "photoURL": imageURLNotSetKey,
            "senderId": senderId!,
            ]

        itemRef.setValue(messageItem)

        JSQSystemSoundPlayer.jsq_playMessageSentSound()

        finishSendingMessage()
        return itemRef.key
    }
    func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
        let itemRef = messageRef.child(key)
        itemRef.updateChildValues(["photoURL": url])
    }
    override func didPressAccessoryButton(_ sender: UIButton) {
        let picker = UIImagePickerController()
        picker.delegate = self as! UIImagePickerControllerDelegate & UINavigationControllerDelegate
        if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
            picker.sourceType = UIImagePickerControllerSourceType.camera
        } else {
            picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
        }

        present(picker, animated: true, completion:nil)
    }

    private func addPhotoMessage(withId id: String, key: String, mediaItem: JSQPhotoMediaItem) {
        if let message = JSQMessage(senderId: id, displayName: "", media: mediaItem) {
            messages.append(message)

            if (mediaItem.image == nil) {
                photoMessageMap[key] = mediaItem
            }

            collectionView.reloadData()
        }
    }

    private func fetchImageDataAtURL(_ photoURL: String, forMediaItem mediaItem: JSQPhotoMediaItem, clearsPhotoMessageMapOnSuccessForKey key: String?) {
        // 1
        let storageRef = FIRStorage.storage().reference(forURL: photoURL)

        // 2
        storageRef.data(withMaxSize: INT64_MAX){ (data, error) in
            if let error = error {
                print("Error downloading image data: \(error)")
                return
            }

            // 3
            storageRef.metadata(completion: { (metadata, metadataErr) in
                if let error = metadataErr {
                    print("Error downloading metadata: \(error)")
                    return
                }

                // 4
                if (metadata?.contentType == "image") {
                    mediaItem.image = UIImage.init(data: data!)
                } else {
                    mediaItem.image = UIImage.init(data: data!)
                }
                self.collectionView.reloadData()

                // 5
                guard key != nil else {
                    return
                }
                self.photoMessageMap.removeValue(forKey: key!)
            })
        }
    }


}

extension SendMessageViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController,
                               didFinishPickingMediaWithInfo info: [String : Any]) {

        picker.dismiss(animated: true, completion:nil)

        // 1
        if let photoReferenceUrl = info[UIImagePickerControllerReferenceURL] as? URL {
            // Handle picking a Photo from the Photo Library
            // 2
            let assets = PHAsset.fetchAssets(withALAssetURLs: [photoReferenceUrl], options: nil)
            let asset = assets.firstObject

            // 3
            if let key = sendPhotoMessage() {
                // 4
                asset?.requestContentEditingInput(with: nil, completionHandler: { (contentEditingInput, info) in
                    let imageFileURL = contentEditingInput?.fullSizeImageURL

                    // 5
                    let path = "\(FIRAuth.auth()?.currentUser?.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\(photoReferenceUrl.lastPathComponent)"

                    // 6
                    self.storageRef.child(path).putFile(imageFileURL!, metadata: nil) { (metadata, error) in
                        if let error = error {
                            print("Error uploading photo: \(error.localizedDescription)")
                            return
                        }
                        // 7
                        self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
                    }
                })
            }
        } else {
            // Handle picking a Photo from the Camera - TODO
            // 1
            let image = info[UIImagePickerControllerOriginalImage] as! UIImage
            // 2
            if let key = sendPhotoMessage() {
                // 3
                let imageData = UIImageJPEGRepresentation(image, 1.0)
                // 4
                let imagePath = FIRAuth.auth()!.currentUser!.uid + "/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
                // 5
                let metadata = FIRStorageMetadata()
                metadata.contentType = "image/jpeg"
                // 6
                storageRef.child(imagePath).put(imageData!, metadata: metadata) { (metadata, error) in
                    if let error = error {
                        print("Error uploading photo: \(error)")
                        return
                    }
                    // 7
                    self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
                }
            }

        }
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion:nil)
    }
}

1 个解决方案

#1


1  

Looks to me like this isn't a privacy issue as you state, it's simply that you're not clearing the data on your messages view controller when you load the new conversation.

在我看来,这并不是一个隐私问题,只是在加载新会话时您没有清除消息视图控制器上的数据。

Ultimately it really depends on how secure you want this to be; if you're happy having the private messages saved in memory, then don't destroy them until the user logs out — you can even keep multiple private conversations saved in a CoreData database. It's still relatively secure this way, and it's convenient for users and performant. If you prefer to destroy messages sooner, clear the data on viewDidDisappear, then checking in your prepareForSegue method that the data is again cleared. You could also destroy the entire messages controller each time you dismiss it, if storing a strong reference isn't what you want to do.

最终,它实际上取决于你想要的安全性;如果您对将私有消息保存在内存中感到高兴,那么在用户注销之前不要销毁它们 - 您甚至可以将多个私有会话保存在CoreData数据库中。这种方式仍然相对安全,对用户和高性能用户来说都很方便。如果您希望尽快销毁消息,请清除viewDidDisappear上的数据,然后检查prepareForSegue方法,以便再次清除数据。如果存储强引用不是您想要的,那么每次解除它时,您也可以销毁整个消息控制器。

An example of this, as a storyboard:

一个例子,作为故事板:

  1. App loads
  2. 应用程序加载
  3. User1 is logged in
  4. User1已登录
  5. User1 selects private messages
  6. User1选择私人消息
  7. User1 has conversation with User2
  8. User1与User2进行对话

  1. User1 switches to a conversation with User3
  2. User1切换到与User3的对话
  3. [pseudo-code]

    [伪代码]

    userDidChangeRecipient {
        // destroy messages view controller
        // or destroy Firebase array data and destroy the reference to the message/conversation ID
    }
    

And each time you load the view controller:

每次加载视图控制器时:

prepareForSegue {
    if strongRefToMessagesVC == nil {
        // instantiate a new instance of vc from nib or scratch
        // load the appropriate message/conversation ID
        // load messages
    }
} 

More digging:

更多挖掘:

There's two possibilities here:

这里有两种可能性:

  1. You're not destroying the view controller when you switch messages, and this tutorial expects you to. In that case, you need to look at when the segue ends or the user closes the messages view controller and either destroy it, or empty the array.

    切换消息时,您不会破坏视图控制器,本教程希望您这样做。在这种情况下,您需要查看segue何时结束或用户关闭消息视图控制器并将其销毁或清空阵列。

  2. You're trying to write all the private messages into the same JSQMessage array. I notice in that view controller you have:

    您正在尝试将所有私有消息写入相同的JSQMessage数组。我在那个视图控制器中注意到你:

    var messageRef = FIRDatabase.database().reference().child("messages")
    

    Is that the database connection you're working with? Each private message conversation should have a unique reference ID so that they do not overlap, otherwise every user will load up the same set of messages from Firebase.

    那是你正在使用的数据库连接吗?每个私人消息对话都应具有唯一的引用ID,以使它们不重叠,否则每个用户都将从Firebase加载同一组消息。

#1


1  

Looks to me like this isn't a privacy issue as you state, it's simply that you're not clearing the data on your messages view controller when you load the new conversation.

在我看来,这并不是一个隐私问题,只是在加载新会话时您没有清除消息视图控制器上的数据。

Ultimately it really depends on how secure you want this to be; if you're happy having the private messages saved in memory, then don't destroy them until the user logs out — you can even keep multiple private conversations saved in a CoreData database. It's still relatively secure this way, and it's convenient for users and performant. If you prefer to destroy messages sooner, clear the data on viewDidDisappear, then checking in your prepareForSegue method that the data is again cleared. You could also destroy the entire messages controller each time you dismiss it, if storing a strong reference isn't what you want to do.

最终,它实际上取决于你想要的安全性;如果您对将私有消息保存在内存中感到高兴,那么在用户注销之前不要销毁它们 - 您甚至可以将多个私有会话保存在CoreData数据库中。这种方式仍然相对安全,对用户和高性能用户来说都很方便。如果您希望尽快销毁消息,请清除viewDidDisappear上的数据,然后检查prepareForSegue方法,以便再次清除数据。如果存储强引用不是您想要的,那么每次解除它时,您也可以销毁整个消息控制器。

An example of this, as a storyboard:

一个例子,作为故事板:

  1. App loads
  2. 应用程序加载
  3. User1 is logged in
  4. User1已登录
  5. User1 selects private messages
  6. User1选择私人消息
  7. User1 has conversation with User2
  8. User1与User2进行对话

  1. User1 switches to a conversation with User3
  2. User1切换到与User3的对话
  3. [pseudo-code]

    [伪代码]

    userDidChangeRecipient {
        // destroy messages view controller
        // or destroy Firebase array data and destroy the reference to the message/conversation ID
    }
    

And each time you load the view controller:

每次加载视图控制器时:

prepareForSegue {
    if strongRefToMessagesVC == nil {
        // instantiate a new instance of vc from nib or scratch
        // load the appropriate message/conversation ID
        // load messages
    }
} 

More digging:

更多挖掘:

There's two possibilities here:

这里有两种可能性:

  1. You're not destroying the view controller when you switch messages, and this tutorial expects you to. In that case, you need to look at when the segue ends or the user closes the messages view controller and either destroy it, or empty the array.

    切换消息时,您不会破坏视图控制器,本教程希望您这样做。在这种情况下,您需要查看segue何时结束或用户关闭消息视图控制器并将其销毁或清空阵列。

  2. You're trying to write all the private messages into the same JSQMessage array. I notice in that view controller you have:

    您正在尝试将所有私有消息写入相同的JSQMessage数组。我在那个视图控制器中注意到你:

    var messageRef = FIRDatabase.database().reference().child("messages")
    

    Is that the database connection you're working with? Each private message conversation should have a unique reference ID so that they do not overlap, otherwise every user will load up the same set of messages from Firebase.

    那是你正在使用的数据库连接吗?每个私人消息对话都应具有唯一的引用ID,以使它们不重叠,否则每个用户都将从Firebase加载同一组消息。